Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion crates/defguard_common/src/db/models/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::fmt;

use chrono::NaiveDateTime;
use model_derive::Model;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use utoipa::ToSchema;

use crate::db::{Id, NoId};

#[derive(Model)]
#[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema, PartialEq)]
pub struct Proxy<I = NoId> {
pub id: I,
pub name: String,
Expand All @@ -18,6 +22,18 @@ pub struct Proxy<I = NoId> {
pub certificate_expiry: Option<NaiveDateTime>,
}

impl fmt::Display for Proxy<NoId> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}

impl fmt::Display for Proxy<Id> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[ID {}] {}", self.id, self.name)
}
}

impl Proxy {
pub fn new<S: Into<String>>(name: S, address: S, port: i32, public_address: S) -> Self {
Self {
Expand Down
7 changes: 7 additions & 0 deletions crates/defguard_core/src/db/models/activity_log/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use defguard_common::db::{
WireguardNetwork,
group::Group,
oauth2client::OAuth2Client,
proxy::Proxy,
settings::{LdapSyncStatus, OpenIdUsernameHandling, SmtpEncryption},
user::User,
},
Expand Down Expand Up @@ -564,3 +565,9 @@ pub struct UserSnatBindingModifiedMetadata {
pub before: UserSnatBinding<Id>,
pub after: UserSnatBinding<Id>,
}

#[derive(Serialize)]
pub struct ProxyModifiedMetadata {
pub before: Proxy<Id>,
pub after: Proxy<Id>,
}
2 changes: 2 additions & 0 deletions crates/defguard_core/src/db/models/activity_log/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ pub enum EventType {
UserSnatBindingAdded,
UserSnatBindingRemoved,
UserSnatBindingModified,
// Proxy management
ProxyModified,
}

#[derive(Model, FromRow, Serialize)]
Expand Down
6 changes: 5 additions & 1 deletion crates/defguard_core/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use defguard_common::db::{
Id,
models::{
AuthenticationKey, Device, MFAMethod, Settings, User, WebAuthn, WireguardNetwork,
group::Group, oauth2client::OAuth2Client,
group::Group, oauth2client::OAuth2Client, proxy::Proxy,
},
};
use defguard_proto::proxy::MfaMethod;
Expand Down Expand Up @@ -297,6 +297,10 @@ pub enum ApiEventType {
before: UserSnatBinding<Id>,
after: UserSnatBinding<Id>,
},
ProxyModified {
before: Proxy<Id>,
after: Proxy<Id>,
},
}

/// Events from Web API
Expand Down
1 change: 1 addition & 0 deletions crates/defguard_core/src/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub mod network_devices;
pub mod openid_clients;
pub mod openid_flow;
pub(crate) mod pagination;
pub mod proxy;
pub(crate) mod settings;
pub(crate) mod ssh_authorized_keys;
pub(crate) mod support;
Expand Down
116 changes: 116 additions & 0 deletions crates/defguard_core/src/handlers/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use axum::{
Json,
extract::{Path, State},
};
use defguard_common::db::models::proxy::Proxy;
use reqwest::StatusCode;
use serde_json::{Value, json};
use utoipa::ToSchema;

use crate::{
appstate::AppState,
auth::{AdminRole, SessionInfo},
events::{ApiEvent, ApiEventType, ApiRequestContext},
handlers::{ApiResponse, ApiResult},
};

#[derive(Serialize, Deserialize, ToSchema)]
pub struct ProxyUpdateData {
pub name: String,
}

#[utoipa::path(
get,
path = "/api/v1/proxy/{proxy_id}",
responses(
(status = 200, description = "Edge details", body = Proxy),
(status = 401, description = "Unauthorized to get edge details.", body = ApiResponse, example = json!({"msg": "Session is required"})),
(status = 403, description = "You don't have permission to get edge details.", body = ApiResponse, example = json!({"msg": "access denied"})),
(status = 404, description = "Edge not found", body = ApiResponse, example = json!({"msg": "network not found"})),
(status = 500, description = "Unable to get edge details.", body = ApiResponse, example = json!({"msg": "Internal server error"}))
),
security(
("cookie" = []),
("api_token" = [])
)
)]
pub(crate) async fn proxy_details(
Path(proxy_id): Path<i64>,
_role: AdminRole,
session: SessionInfo,
State(appstate): State<AppState>,
) -> ApiResult {
debug!(
"User {} displaying details for proxy {proxy_id}",
session.user.username
);
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,
},
};
info!(
"User {} displayed details for proxy {proxy_id}",
session.user.username
);

Ok(response)
}

#[utoipa::path(
put,
path = "/api/v1/proxy/{proxy_id}",
request_body = Proxy,
responses(
(status = 200, description = "Successfully modified edge.", body = ProxyUpdateData),
(status = 401, description = "Unauthorized to modify edge.", body = ApiResponse, example = json!({"msg": "Session is required"})),
(status = 403, description = "You don't have permission to modify 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 modify edge.", body = ApiResponse, example = json!({"msg": "Internal server error"}))
),
security(
("cookie" = []),
("api_token" = [])
)
)]
pub(crate) async fn update_proxy(
_role: AdminRole,
Path(proxy_id): Path<i64>,
State(appstate): State<AppState>,
session: SessionInfo,
context: ApiRequestContext,
Json(data): Json<ProxyUpdateData>,
) -> ApiResult {
debug!("User {} updating proxy {proxy_id}", session.user.username);
let proxy = Proxy::find_by_id(&appstate.pool, proxy_id).await?;

let Some(mut proxy) = proxy else {
warn!("Proxy {proxy_id} not found");
return Ok(ApiResponse {
json: Value::Null,
status: StatusCode::NOT_FOUND,
});
};
let before = proxy.clone();

proxy.name = data.name;
proxy.save(&appstate.pool).await?;

info!("User {} updated proxy {proxy_id}", session.user.username);

appstate.emit_event(ApiEvent {
context,
event: Box::new(ApiEventType::ProxyModified {
before,
after: proxy.clone(),
}),
})?;

Ok(ApiResponse::json(proxy, StatusCode::OK))
}
3 changes: 3 additions & 0 deletions crates/defguard_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ use crate::{
authorization, discovery_keys, openid_configuration, secure_authorization, token,
userinfo,
},
proxy::{proxy_details, update_proxy},
settings::{
get_settings, get_settings_essentials, patch_settings, set_default_branding,
test_ldap_settings, update_settings,
Expand Down Expand Up @@ -359,6 +360,8 @@ pub fn build_webapp(
.route("/activity_log", get(get_activity_log_events))
// Certificate authority
.route("/ca", post(create_ca))
// Proxy routes
.route("/proxy/{proxy_id}", get(proxy_details).put(update_proxy))
// Proxy setup with SSE
.route("/proxy/setup/stream", get(setup_proxy_tls_stream)),
);
Expand Down
1 change: 1 addition & 0 deletions crates/defguard_core/tests/integration/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod group;
mod oauth;
mod openid;
mod openid_login;
mod proxy;
mod settings;
mod snat;
mod user;
Expand Down
55 changes: 55 additions & 0 deletions crates/defguard_core/tests/integration/api/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use defguard_common::db::{Id, models::proxy::Proxy};
use defguard_core::handlers::{Auth, proxy::ProxyUpdateData};
use reqwest::StatusCode;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};

use super::common::{make_test_client, setup_pool};

#[sqlx::test]
async fn test_update_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 mut proxy = Proxy::new("test", "localhost", 50051, "public.net")
.save(&pool)
.await
.unwrap();

// Modify name
let data = ProxyUpdateData {
name: "modified".to_string(),
};
let response = client
.put(format!("/api/v1/proxy/{}", proxy.id))
.json(&data)
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);

// Verify proxy is modified correctly
let proxy_updated: Proxy<Id> = response.json().await;
assert_eq!(proxy_updated.name, "modified");
proxy.name = "modified".to_string();
assert_eq!(proxy, proxy_updated);

// Try to modify other fields
let proxy_before_mods = proxy.clone();
proxy.address = "otherhost".to_string();
proxy.port = 50052;
proxy.public_address = "otherpublichost.net".to_string();
let response = client
.put(format!("/api/v1/proxy/{}", proxy.id))
.json(&proxy)
.send()
.await;
assert_eq!(response.status(), StatusCode::OK);
let proxy_updated: Proxy<Id> = response.json().await;
assert_eq!(proxy_before_mods, proxy_updated);
}
3 changes: 3 additions & 0 deletions crates/defguard_event_logger/src/description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ pub fn get_defguard_event_description(event: &DefguardEvent) -> Option<String> {
"Public IP bound to devices owned by user {user} changed from {} to {}",
before.public_ip, after.public_ip
)),
DefguardEvent::ProxyModified { before: _, after } => {
Some(format!("Modified proxy {after}"))
}
}
}

Expand Down
14 changes: 9 additions & 5 deletions crates/defguard_event_logger/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ use defguard_core::db::models::activity_log::{
MfaSecurityKeyMetadata, NetworkDeviceMetadata, NetworkDeviceModifiedMetadata,
OpenIdAppMetadata, OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata,
OpenIdProviderMetadata, PasswordChangedByAdminMetadata, PasswordResetMetadata,
SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata, UserMfaDisabledMetadata,
UserModifiedMetadata, UserSnatBindingMetadata, UserSnatBindingModifiedMetadata,
VpnClientMetadata, VpnClientMfaFailedMetadata, VpnClientMfaMetadata, VpnLocationMetadata,
VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata,
WebHookStateChangedMetadata,
ProxyModifiedMetadata, SettingsUpdateMetadata, UserGroupsModifiedMetadata, UserMetadata,
UserMfaDisabledMetadata, UserModifiedMetadata, UserSnatBindingMetadata,
UserSnatBindingModifiedMetadata, VpnClientMetadata, VpnClientMfaFailedMetadata,
VpnClientMfaMetadata, VpnLocationMetadata, VpnLocationModifiedMetadata, WebHookMetadata,
WebHookModifiedMetadata, WebHookStateChangedMetadata,
},
};
use description::{
Expand Down Expand Up @@ -468,6 +468,10 @@ pub async fn run_event_logger(
})
.ok(),
),
DefguardEvent::ProxyModified { before, after } => (
EventType::ProxyModified,
serde_json::to_value(ProxyModifiedMetadata { before, after }).ok(),
),
};
(module, event_type, description, metadata)
}
Expand Down
6 changes: 5 additions & 1 deletion crates/defguard_event_logger/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use defguard_common::db::{
Id,
models::{
AuthenticationKey, Device, MFAMethod, Settings, User, WebAuthn, WireguardNetwork,
group::Group, oauth2client::OAuth2Client,
group::Group, oauth2client::OAuth2Client, proxy::Proxy,
},
};
use defguard_core::{
Expand Down Expand Up @@ -337,6 +337,10 @@ pub enum DefguardEvent {
before: UserSnatBinding<Id>,
after: UserSnatBinding<Id>,
},
ProxyModified {
before: Proxy<Id>,
after: Proxy<Id>,
},
}

/// Represents activity log events related to client applications
Expand Down
4 changes: 4 additions & 0 deletions crates/defguard_event_router/src/handlers/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,10 @@ impl EventRouter {
})),
Some(location),
),
ApiEventType::ProxyModified { before, after } => (
LoggerEvent::Defguard(Box::new(DefguardEvent::ProxyModified { before, after })),
None,
),
};
self.log_event(
EventContext::from_api_context(event.context, location),
Expand Down
13 changes: 13 additions & 0 deletions web/messages/en/edge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"edge_title": "Edge components",
"edge_edit_title": "Edit edge component",
"edge_edit_general_info": "General information",
"edge_edit_name": "Name",
"edge_edit_address": "IP or Domain",
"edge_edit_port": "gRPC port",
"edge_edit_public_address": "Public domain",
"edge_edit_delete": "Delete",
"edge_edit_success": "Edge component updated",
"edge_edit_failed": "Failed to update edge component"
}
1 change: 1 addition & 0 deletions web/project.inlang/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"./messages/{locale}/groups.json",
"./messages/{locale}/openid.json",
"./messages/{locale}/activity.json",
"./messages/{locale}/edge.json",
"./messages/{locale}/edge_wizard.json",
"./messages/{locale}/settings.json",
"./messages/{locale}/gateway_wizard.json"
Expand Down
11 changes: 11 additions & 0 deletions web/src/pages/EdgeListPage/EdgeListPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { m } from '../../paraglide/messages';
import { Page } from '../../shared/components/Page/Page';
import { TablePageLayout } from '../../shared/layout/TablePageLayout/TablePageLayout';

export const EdgeListPage = () => {
return (
<Page title={m.edge_title()}>
<TablePageLayout>TODO</TablePageLayout>
</Page>
);
};
Loading
Loading