diff --git a/crates/defguard_core/src/db/models/activity_log/metadata.rs b/crates/defguard_core/src/db/models/activity_log/metadata.rs index 406c821b95..3d13c68c94 100644 --- a/crates/defguard_core/src/db/models/activity_log/metadata.rs +++ b/crates/defguard_core/src/db/models/activity_log/metadata.rs @@ -571,3 +571,8 @@ pub struct ProxyModifiedMetadata { pub before: Proxy, pub after: Proxy, } + +#[derive(Serialize)] +pub struct ProxyDeletedMetadata { + pub proxy: Proxy, +} diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 25f9def4c9..4d756ed820 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -301,6 +301,9 @@ pub enum ApiEventType { before: Proxy, after: Proxy, }, + ProxyDeleted { + proxy: Proxy, + }, } /// Events from Web API diff --git a/crates/defguard_core/src/handlers/proxy.rs b/crates/defguard_core/src/handlers/proxy.rs index 6ae43d593d..ebbfa5efe0 100644 --- a/crates/defguard_core/src/handlers/proxy.rs +++ b/crates/defguard_core/src/handlers/proxy.rs @@ -2,9 +2,9 @@ use axum::{ Json, extract::{Path, State}, }; -use defguard_common::db::models::proxy::Proxy; +use defguard_common::{db::models::proxy::Proxy, types::proxy::ProxyControlMessage}; use reqwest::StatusCode; -use serde_json::{Value, json}; +use serde_json::Value; use utoipa::ToSchema; use crate::{ @@ -46,14 +46,8 @@ pub(crate) async fn proxy_details( ); let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; let response = match proxy { - Some(proxy) => ApiResponse { - json: json!(proxy), - status: StatusCode::OK, - }, - None => ApiResponse { - json: Value::Null, - status: StatusCode::NOT_FOUND, - }, + Some(proxy) => ApiResponse::json(proxy, StatusCode::OK), + None => ApiResponse::json(Value::Null, StatusCode::NOT_FOUND), }; info!( "User {} displayed details for proxy {proxy_id}", @@ -92,10 +86,7 @@ pub(crate) async fn update_proxy( let Some(mut proxy) = proxy else { warn!("Proxy {proxy_id} not found"); - return Ok(ApiResponse { - json: Value::Null, - status: StatusCode::NOT_FOUND, - }); + return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND)); }; let before = proxy.clone(); @@ -114,3 +105,61 @@ pub(crate) async fn update_proxy( Ok(ApiResponse::json(proxy, StatusCode::OK)) } + +#[utoipa::path( + delete, + path = "/api/v1/proxy/{proxy_id}", + request_body = Proxy, + responses( + (status = 200, description = "Successfully deleted edge.", body = ApiResponse), + (status = 401, description = "Unauthorized to delete edge.", body = ApiResponse, example = json!({"msg": "Session is required"})), + (status = 403, description = "You don't have permission delete an edge.", body = ApiResponse, example = json!({"msg": "access denied"})), + (status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "proxy not found"})), + (status = 500, description = "Unable to delete edge.", body = ApiResponse, example = json!({"msg": "Internal server error"})) + ), + security( + ("cookie" = []), + ("api_token" = []) + ) +)] +pub(crate) async fn delete_proxy( + _role: AdminRole, + Path(proxy_id): Path, + State(appstate): State, + session: SessionInfo, + context: ApiRequestContext, +) -> ApiResult { + debug!("User {} deleteing proxy {proxy_id}", session.user.username); + let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?; + + let Some(proxy) = proxy else { + warn!("Proxy {proxy_id} not found"); + return Ok(ApiResponse::json(Value::Null, StatusCode::NOT_FOUND)); + }; + + // Disconnect the proxy + if let Err(err) = appstate + .proxy_control_tx + .send(ProxyControlMessage::ShutdownConnection(proxy.id)) + .await + { + error!( + "Error shutting down proxy {}, it may be disconnected: {err:?}", + proxy.id + ); + } + + // TODO + // 1. Add proxy cert to CRL + // 2. Remove cert files on deleted proxy + proxy.clone().delete(&appstate.pool).await?; + + info!("User {} deleted proxy {proxy_id}", session.user.username); + + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ProxyDeleted { proxy }), + })?; + + Ok(ApiResponse::default()) +} diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 4bcdf7d2a7..c4a153a83a 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -132,7 +132,7 @@ use crate::{ authorization, discovery_keys, openid_configuration, secure_authorization, token, userinfo, }, - proxy::{proxy_details, update_proxy}, + proxy::{delete_proxy, proxy_details, update_proxy}, settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, @@ -359,7 +359,10 @@ pub fn build_webapp( // Certificate authority .route("/ca", post(create_ca)) // Proxy routes - .route("/proxy/{proxy_id}", get(proxy_details).put(update_proxy)) + .route( + "/proxy/{proxy_id}", + get(proxy_details).put(update_proxy).delete(delete_proxy), + ) // Proxy setup with SSE .route("/proxy/setup/stream", get(setup_proxy_tls_stream)), ); diff --git a/crates/defguard_core/tests/integration/api/proxy.rs b/crates/defguard_core/tests/integration/api/proxy.rs index ba4ba23c85..0cead7ec97 100644 --- a/crates/defguard_core/tests/integration/api/proxy.rs +++ b/crates/defguard_core/tests/integration/api/proxy.rs @@ -6,7 +6,36 @@ use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::common::{make_test_client, setup_pool}; #[sqlx::test] -async fn test_update_proxy(_: PgPoolOptions, options: PgConnectOptions) { +async fn test_proxy_details(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, _) = make_test_client(pool.clone()).await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new proxy. + let proxy = Proxy::new("test", "localhost", 50051, "public.net") + .save(&pool) + .await + .unwrap(); + + // Get proxy via API + let response = client + .get(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Verify proxy is correct + let proxy_from_api: Proxy = response.json().await; + assert_eq!(proxy, proxy_from_api); +} + +#[sqlx::test] +async fn test_proxy_update(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let (client, _) = make_test_client(pool.clone()).await; @@ -53,3 +82,36 @@ async fn test_update_proxy(_: PgPoolOptions, options: PgConnectOptions) { let proxy_updated: Proxy = response.json().await; assert_eq!(proxy_before_mods, proxy_updated); } + +#[sqlx::test] +async fn test_delete_proxy(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, _) = make_test_client(pool.clone()).await; + + // Authorize as an administrator. + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // Create new proxy. + let proxy = Proxy::new("test", "localhost", 50051, "public.net") + .save(&pool) + .await + .unwrap(); + + // Delete proxy via API + let response = client + .delete(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::OK); + + // Verify proxy is deleted + let response = client + .get(format!("/api/v1/proxy/{}", proxy.id)) + .send() + .await; + assert_eq!(response.status(), StatusCode::NOT_FOUND); + assert_eq!(Proxy::all(&pool).await.unwrap().len(), 0); +} diff --git a/crates/defguard_event_logger/src/description.rs b/crates/defguard_event_logger/src/description.rs index 9e283d59da..2a666a6009 100644 --- a/crates/defguard_event_logger/src/description.rs +++ b/crates/defguard_event_logger/src/description.rs @@ -260,6 +260,7 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option { DefguardEvent::ProxyModified { before: _, after } => { Some(format!("Modified proxy {after}")) } + DefguardEvent::ProxyDeleted { proxy } => Some(format!("Deleted proxy {proxy}")), } } diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 949e21f97f..3bfec44202 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -12,11 +12,12 @@ use defguard_core::db::models::activity_log::{ MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata, - ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, - UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata, - UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata, - VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata, - WebHookModifiedMetadata, WebHookStateChangedMetadata, + ProxyDeletedMetadata, ProxyModifiedMetadata, SettingsUpdateMetadata, + UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata, UserModifiedMetadata, + UserSnatBindingMetadata, UserSnatBindingModifiedMetadata, VpnClientMetadata, + VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata, + VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, + WebHookStateChangedMetadata, }, }; use description::{ @@ -472,6 +473,10 @@ pub async fn run_event_logger( EventType::ProxyModified, serde_json::to_value(ProxyModifiedMetadata { before, after }).ok(), ), + DefguardEvent::ProxyDeleted { proxy } => ( + EventType::ProxyModified, + serde_json::to_value(ProxyDeletedMetadata { proxy }).ok(), + ), }; (module, event_type, description, metadata) } diff --git a/crates/defguard_event_logger/src/message.rs b/crates/defguard_event_logger/src/message.rs index 4a95201335..651c63cf09 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -321,6 +321,9 @@ pub enum DefguardEvent { before: Proxy, after: Proxy, }, + ProxyDeleted { + proxy: Proxy, + }, } /// Represents activity log events related to client applications diff --git a/crates/defguard_event_router/src/handlers/api.rs b/crates/defguard_event_router/src/handlers/api.rs index d4208b29bd..d8ebe1644b 100644 --- a/crates/defguard_event_router/src/handlers/api.rs +++ b/crates/defguard_event_router/src/handlers/api.rs @@ -393,6 +393,10 @@ impl EventRouter { LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyModified { before, after })), None, ), + ApiEventType::ProxyDeleted { proxy } => ( + LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyDeleted { proxy })), + None, + ), }; self.log_event( EventContext::from_api_context(event.context, location), diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index b58877883f..0407050bda 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -9,5 +9,7 @@ "edge_edit_public_address": "Public domain", "edge_edit_delete": "Delete", "edge_edit_success": "Edge component updated", - "edge_edit_failed": "Failed to update edge component" + "edge_edit_failed": "Failed to update edge component", + "edge_delete_success": "Edge component deleted", + "edge_delete_failed": "Failed to delete edge component" } diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 7c39c4659f..d83fe85812 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -74,6 +74,10 @@ const EditEdgeForm = ({ edge }: { edge: Edge }) => { to: '/edge', replace: true, }); + Snackbar.success(m.edge_delete_success()); + }, + onError: () => { + Snackbar.error(m.edge_delete_failed()); }, });