From 47e0e19273167d9d578243a5d00d26079e470607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 12 Jun 2025 12:56:21 +0200 Subject: [PATCH 1/6] align proto branch --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 2f37d804d..20fe30dfa 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 2f37d804d074c68842262b55c78e536a464c4953 +Subproject commit 20fe30dfa1c2985bb7a6afe1c74dd9a709e034c6 From 99d626aff9654730308d4e8519047b310dc97e2b Mon Sep 17 00:00:00 2001 From: Maciek <19913370+wojcik91@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:27:14 +0200 Subject: [PATCH 2/6] Pass admin device management flag in enrollment start response (#1235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pass device management flag in enrollment settings * return error if device management is disabled * add admin flag to enrollment response * update protos * update protos --------- Co-authored-by: Maciej Wójcik --- crates/defguard_core/src/grpc/enrollment.rs | 50 +++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs index 046542ab9..c57ccc382 100644 --- a/crates/defguard_core/src/grpc/enrollment.rs +++ b/crates/defguard_core/src/grpc/enrollment.rs @@ -227,9 +227,10 @@ impl EnrollmentServer { error!("Failed to get enterprise settings: {err}"); Status::internal("unexpected error") })?; - let enrollment_settings = super::proto::proxy::Settings { + let enrollment_settings = super::proto::proxy::EnrollmentSettings { vpn_setup_optional, only_client_activation: enterprise_settings.only_client_activation, + admin_device_management: enterprise_settings.admin_device_management, }; let response = super::proto::proxy::EnrollmentStartResponse { admin: admin_info, @@ -398,6 +399,34 @@ impl EnrollmentServer { // fetch related users let user = enrollment_token.fetch_user(&self.pool).await?; + // check if adding device by non-admin users is allowed + debug!( + "Fetching enterprise settings for device creation process for user {}({:?})", + user.username, user.id, + ); + let enterprise_settings = EnterpriseSettings::get(&self.pool).await.map_err(|err| { + error!( + "Failed to fetch enterprise settings for device creation process for user {}({:?}): \ + {err}", + user.username, user.id, + ); + Status::internal("unexpected error") + })?; + debug!("Enterprise settings: {enterprise_settings:?}"); + + if !user.is_admin(&self.pool).await.map_err(|err| { + error!( + "Failed to fetch admin status for user {}({:?}): {err}", + user.username, user.id, + ); + Status::internal("unexpected error") + })? && enterprise_settings.admin_device_management + { + return Err(Status::invalid_argument( + "only admin users can manage devices", + )); + } + // add device debug!( "Verifying if user {}({:?}) is active", @@ -627,23 +656,6 @@ impl EnrollmentServer { let settings = Settings::get_current_settings(); debug!("Settings: {settings:?}"); - debug!( - "Fetching enterprise settings for device {} creation process for user {}({:?})", - device.wireguard_pubkey, user.username, user.id, - ); - let enterprise_settings = - EnterpriseSettings::get(&mut *transaction) - .await - .map_err(|err| { - error!( - "Failed to fetch enterprise settings for device {} creation process for user {}({:?}): \ - {err}", - device.wireguard_pubkey, user.username, user.id, - ); - Status::internal("unexpected error") - })?; - debug!("Enterprise settings: {enterprise_settings:?}"); - // create polling token for further client communication debug!( "Creating polling token for further client communication for device {}, user {}({:?})", @@ -758,6 +770,7 @@ impl InitialUserInfo { let enrolled = user.is_enrolled(); let devices = user.user_devices(pool).await?; let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); + let is_admin = user.is_admin(pool).await?; Ok(Self { first_name: user.first_name, last_name: user.last_name, @@ -767,6 +780,7 @@ impl InitialUserInfo { is_active: user.is_active, device_names, enrolled, + is_admin, }) } } From d1fbb071eb29909f18d8d3f587c58ca10a532d7b Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Mon, 23 Jun 2025 10:08:39 +0200 Subject: [PATCH 3/6] Implement remaining activity-log event types (#1243) * VPN crud events * Emit VpnLocationAdded event when importing from file * ApiToken event handling * OpenId client app events * OpenIdProvider events * Settings events * Restore default settings event * Group management events * WebHook events * Webauthn key management events * AuthenticationKey events * Password change, enrollment events * Translation * Store whole objects in the events * Don't store secrets in metadata * Implement UserNoSecrets metadata struct * Update sqlx query data * Remove skip_serializing marker * Box event enums to avoid big size differences between variants * Allow large enum variant on GatewayServerError * Don't box whole events * Remove todo comment * Remove client events --- ...dc105a0411bd634a080391e41f431f966c17.json} | 4 +- .../src/db/models/audit_log/metadata.rs | 361 +++++++++++++-- .../src/db/models/audit_log/mod.rs | 33 ++ .../src/db/models/authentication_key.rs | 14 +- crates/defguard_core/src/db/models/group.rs | 2 +- .../src/db/models/oauth2client.rs | 2 +- .../defguard_core/src/db/models/webauthn.rs | 6 +- crates/defguard_core/src/db/models/webhook.rs | 2 +- .../src/enterprise/db/models/api_tokens.rs | 4 +- .../src/enterprise/db/models/audit_stream.rs | 2 +- .../enterprise/db/models/openid_provider.rs | 4 +- .../src/enterprise/handlers/api_tokens.rs | 47 +- .../src/enterprise/handlers/audit_stream.rs | 30 +- .../enterprise/handlers/openid_providers.rs | 62 +-- crates/defguard_core/src/events.rs | 188 ++++++-- .../src/grpc/desktop_client_mfa.rs | 21 +- crates/defguard_core/src/grpc/enrollment.rs | 2 +- crates/defguard_core/src/grpc/gateway/mod.rs | 2 +- .../defguard_core/src/grpc/password_reset.rs | 2 +- crates/defguard_core/src/handlers/auth.rs | 58 +-- crates/defguard_core/src/handlers/group.rs | 38 +- .../src/handlers/network_devices.rs | 43 +- .../src/handlers/openid_clients.rs | 71 ++- crates/defguard_core/src/handlers/settings.rs | 16 + .../src/handlers/ssh_authorized_keys.rs | 33 +- crates/defguard_core/src/handlers/user.rs | 54 ++- crates/defguard_core/src/handlers/webhooks.rs | 51 ++- .../defguard_core/src/handlers/wireguard.rs | 100 +++-- crates/defguard_event_logger/src/lib.rs | 421 +++++++++++------- crates/defguard_event_logger/src/message.rs | 240 +++++----- crates/defguard_event_router/src/events.rs | 3 - .../defguard_event_router/src/handlers/api.rs | 306 ++++++++----- .../src/handlers/bidi.rs | 28 +- .../src/handlers/grpc.rs | 7 +- .../src/handlers/internal.rs | 5 +- crates/defguard_event_router/src/lib.rs | 2 +- web/src/i18n/en/index.ts | 34 +- web/src/i18n/i18n-types.ts | 260 ++++++++++- web/src/pages/activity/types.ts | 66 ++- 39 files changed, 1913 insertions(+), 711 deletions(-) rename .sqlx/{query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json => query-770fcf951f69a40e2e9833486425dc105a0411bd634a080391e41f431f966c17.json} (85%) diff --git a/.sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json b/.sqlx/query-770fcf951f69a40e2e9833486425dc105a0411bd634a080391e41f431f966c17.json similarity index 85% rename from .sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json rename to .sqlx/query-770fcf951f69a40e2e9833486425dc105a0411bd634a080391e41f431f966c17.json index c7a2ba03b..418350329 100644 --- a/.sqlx/query-0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2.json +++ b/.sqlx/query-770fcf951f69a40e2e9833486425dc105a0411bd634a080391e41f431f966c17.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, \n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match FROM openidprovider WHERE name = $1", + "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled,\n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match FROM openidprovider WHERE name = $1", "describe": { "columns": [ { @@ -147,5 +147,5 @@ false ] }, - "hash": "0d0ed874821849ae07a9f49f17600b2a4cbedb33babd5b9fc908ec17d3f882e2" + "hash": "770fcf951f69a40e2e9833486425dc105a0411bd634a080391e41f431f966c17" } diff --git a/crates/defguard_core/src/db/models/audit_log/metadata.rs b/crates/defguard_core/src/db/models/audit_log/metadata.rs index 10cfe8668..141b7cc48 100644 --- a/crates/defguard_core/src/db/models/audit_log/metadata.rs +++ b/crates/defguard_core/src/db/models/audit_log/metadata.rs @@ -1,4 +1,19 @@ -use crate::db::{Device, Id, MFAMethod, WireguardNetwork}; +use chrono::NaiveDateTime; + +use crate::{ + db::{ + models::{ + authentication_key::{AuthenticationKey, AuthenticationKeyType}, + oauth2client::OAuth2Client, + }, + Device, Group, Id, MFAMethod, User, WebAuthn, WebHook, WireguardNetwork, + }, + enterprise::db::models::{ + api_tokens::ApiToken, + audit_stream::{AuditStream, AuditStreamType}, + openid_provider::{DirectorySyncTarget, DirectorySyncUserBehavior, OpenIdProvider}, + }, +}; #[derive(Serialize)] pub struct MfaLoginMetadata { @@ -6,75 +21,132 @@ pub struct MfaLoginMetadata { } #[derive(Serialize)] -pub struct DeviceAddedMetadata { - pub device_names: Vec, +pub struct UserNoSecrets { + pub id: Id, + pub username: String, + pub last_name: String, + pub first_name: String, + pub email: String, + pub phone: Option, + pub mfa_enabled: bool, + pub is_active: bool, + pub from_ldap: bool, + pub ldap_pass_randomized: bool, + pub ldap_rdn: Option, + pub openid_sub: Option, + pub totp_enabled: bool, + pub email_mfa_enabled: bool, + pub mfa_method: MFAMethod, } -#[derive(Serialize)] -pub struct DeviceRemovedMetadata { - pub device_names: Vec, +impl From> for UserNoSecrets { + fn from(value: User) -> Self { + Self { + id: value.id, + username: value.username, + last_name: value.last_name, + first_name: value.first_name, + email: value.email, + phone: value.phone, + mfa_enabled: value.mfa_enabled, + is_active: value.is_active, + from_ldap: value.from_ldap, + ldap_pass_randomized: value.ldap_pass_randomized, + ldap_rdn: value.ldap_rdn, + openid_sub: value.openid_sub, + totp_enabled: value.totp_enabled, + email_mfa_enabled: value.email_mfa_enabled, + mfa_method: value.mfa_method, + } + } } #[derive(Serialize)] -pub struct DeviceModifiedMetadata { - pub device_names: Vec, +pub struct DeviceMetadata { + pub owner: UserNoSecrets, + pub device: Device, } #[derive(Serialize)] -pub struct NetworkDeviceAddedMetadata { - pub device_id: Id, - pub device_name: String, - pub location_id: Id, - pub location: String, +pub struct DeviceModifiedMetadata { + pub owner: UserNoSecrets, + pub before: Device, + pub after: Device, } #[derive(Serialize)] -pub struct NetworkDeviceRemovedMetadata { - pub device_id: Id, - pub device_name: String, - pub location_id: Id, - pub location: String, +pub struct NetworkDeviceMetadata { + pub device: Device, + pub location: WireguardNetwork, } #[derive(Serialize)] pub struct NetworkDeviceModifiedMetadata { - pub device_id: Id, - pub device_name: String, - pub location_id: Id, - pub location: String, + pub location: WireguardNetwork, + pub before: Device, + pub after: Device, } #[derive(Serialize)] -pub struct UserAddedMetadata { - pub username: String, +pub struct UserMetadata { + pub user: UserNoSecrets, } #[derive(Serialize)] pub struct UserModifiedMetadata { - pub username: String, + pub before: UserNoSecrets, + pub after: UserNoSecrets, } #[derive(Serialize)] -pub struct UserRemovedMetadata { - pub username: String, +pub struct MfaSecurityKeyMetadata { + pub key: WebAuthnNoSecrets, } +// Avoid storing secrets in metadata #[derive(Serialize)] -pub struct MfaSecurityKeyRemovedMetadata { - pub key_id: Id, - pub key_name: String, +pub struct WebAuthnNoSecrets { + pub id: Id, + pub user_id: Id, + pub name: String, } -#[derive(Serialize)] -pub struct MfaSecurityKeyAddedMetadata { - pub key_id: Id, - pub key_name: String, +impl From> for WebAuthnNoSecrets { + fn from(value: WebAuthn) -> Self { + Self { + id: value.id, + user_id: value.user_id, + name: value.name, + } + } } #[derive(Serialize)] pub struct AuditStreamMetadata { + pub stream: AuditStreamNoSecrets, +} + +#[derive(Serialize)] +pub struct AuditStreamNoSecrets { pub id: Id, pub name: String, + pub stream_type: AuditStreamType, +} + +impl From> for AuditStreamNoSecrets { + fn from(value: AuditStream) -> Self { + Self { + id: value.id, + name: value.name, + stream_type: value.stream_type, + } + } +} + +#[derive(Serialize)] +pub struct AuditStreamModifiedMetadata { + pub before: AuditStreamNoSecrets, + pub after: AuditStreamNoSecrets, } #[derive(Serialize)] @@ -94,3 +166,224 @@ pub struct VpnClientMfaMetadata { pub struct EnrollmentDeviceAddedMetadata { pub device: Device, } + +#[derive(Serialize)] +pub struct EnrollmentTokenMetadata { + pub user: UserNoSecrets, +} + +#[derive(Serialize)] +pub struct VpnLocationMetadata { + pub location: WireguardNetwork, +} + +#[derive(Serialize)] +pub struct VpnLocationModifiedMetadata { + pub before: WireguardNetwork, + pub after: WireguardNetwork, +} + +#[derive(Serialize)] +pub struct ApiTokenMetadata { + pub owner: UserNoSecrets, + pub token: ApiTokenNoSecrets, +} + +#[derive(Serialize)] +pub struct ApiTokenNoSecrets { + id: Id, + pub user_id: Id, + pub created_at: NaiveDateTime, + pub name: String, +} + +impl From> for ApiTokenNoSecrets { + fn from(value: ApiToken) -> Self { + Self { + id: value.id, + user_id: value.user_id, + created_at: value.created_at, + name: value.name, + } + } +} + +#[derive(Serialize)] +pub struct ApiTokenRenamedMetadata { + pub owner: UserNoSecrets, + pub token: ApiTokenNoSecrets, + pub old_name: String, + pub new_name: String, +} + +#[derive(Serialize)] +pub struct OpenIdAppMetadata { + pub app: OAuth2ClientNoSecrets, +} + +#[derive(Serialize)] +pub struct OAuth2ClientNoSecrets { + pub id: Id, + pub client_id: String, // unique + pub redirect_uri: Vec, + pub scope: Vec, + pub name: String, + pub enabled: bool, +} + +impl From> for OAuth2ClientNoSecrets { + fn from(value: OAuth2Client) -> Self { + Self { + id: value.id, + client_id: value.client_id, + redirect_uri: value.redirect_uri, + scope: value.scope, + name: value.name, + enabled: value.enabled, + } + } +} + +#[derive(Serialize)] +pub struct OpenIdAppModifiedMetadata { + pub before: OAuth2ClientNoSecrets, + pub after: OAuth2ClientNoSecrets, +} + +#[derive(Serialize)] +pub struct OpenIdAppStateChangedMetadata { + pub app: OAuth2ClientNoSecrets, + pub enabled: bool, +} + +#[derive(Serialize)] +pub struct OpenIdProviderMetadata { + pub provider: OpenIdProviderNoSecrets, +} + +#[derive(Serialize)] +pub struct OpenIdProviderNoSecrets { + pub id: Id, + pub name: String, + pub base_url: String, + pub client_id: String, + pub display_name: Option, + pub google_service_account_email: Option, + pub admin_email: Option, + pub directory_sync_enabled: bool, + pub directory_sync_interval: i32, + pub directory_sync_user_behavior: DirectorySyncUserBehavior, + pub directory_sync_admin_behavior: DirectorySyncUserBehavior, + pub directory_sync_target: DirectorySyncTarget, + pub okta_dirsync_client_id: Option, + pub directory_sync_group_match: Vec, +} + +impl From> for OpenIdProviderNoSecrets { + fn from(value: OpenIdProvider) -> Self { + Self { + id: value.id, + name: value.name, + base_url: value.base_url, + client_id: value.client_id, + display_name: value.display_name, + google_service_account_email: value.google_service_account_email, + admin_email: value.admin_email, + directory_sync_enabled: value.directory_sync_enabled, + directory_sync_interval: value.directory_sync_interval, + directory_sync_user_behavior: value.directory_sync_user_behavior, + directory_sync_admin_behavior: value.directory_sync_admin_behavior, + directory_sync_target: value.directory_sync_target, + okta_dirsync_client_id: value.okta_dirsync_client_id, + directory_sync_group_match: value.directory_sync_group_match, + } + } +} + +#[derive(Serialize)] +pub struct GroupsBulkAssignedMetadata { + pub users: Vec, + pub groups: Vec>, +} + +#[derive(Serialize)] +pub struct GroupMetadata { + pub group: Group, +} + +#[derive(Serialize)] +pub struct GroupModifiedMetadata { + pub before: Group, + pub after: Group, +} + +#[derive(Serialize)] +pub struct GroupAssignedMetadata { + pub group: Group, + pub user: UserNoSecrets, +} + +#[derive(Serialize)] +pub struct WebHookMetadata { + pub webhook: WebHook, +} + +#[derive(Serialize)] +pub struct WebHookModifiedMetadata { + pub before: WebHook, + pub after: WebHook, +} + +#[derive(Serialize)] +pub struct WebHookStateChangedMetadata { + pub webhook: WebHook, + pub enabled: bool, +} + +#[derive(Serialize)] +pub struct AuthenticationKeyMetadata { + pub key: AuthenticationKeyNoSecrets, +} + +#[derive(Serialize)] +pub struct AuthenticationKeyNoSecrets { + pub id: Id, + pub yubikey_id: Option, + pub name: Option, + pub user_id: Id, + pub key_type: AuthenticationKeyType, +} + +impl From> for AuthenticationKeyNoSecrets { + fn from(value: AuthenticationKey) -> Self { + Self { + id: value.id, + yubikey_id: value.yubikey_id, + name: value.name, + user_id: value.user_id, + key_type: value.key_type, + } + } +} + +#[derive(Serialize)] +pub struct AuthenticationKeyRenamedMetadata { + pub key: AuthenticationKeyNoSecrets, + pub old_name: Option, + pub new_name: Option, +} + +#[derive(Serialize)] +pub struct PasswordChangedByAdminMetadata { + pub user: UserNoSecrets, +} + +#[derive(Serialize)] +pub struct PasswordResetMetadata { + pub user: UserNoSecrets, +} + +#[derive(Serialize)] +pub struct ClientConfigurationTokenMetadata { + pub user: UserNoSecrets, +} diff --git a/crates/defguard_core/src/db/models/audit_log/mod.rs b/crates/defguard_core/src/db/models/audit_log/mod.rs index 0e4d6f24a..0cf327d92 100644 --- a/crates/defguard_core/src/db/models/audit_log/mod.rs +++ b/crates/defguard_core/src/db/models/audit_log/mod.rs @@ -43,6 +43,9 @@ pub enum EventType { UserAdded, UserRemoved, UserModified, + PasswordChanged, + PasswordChangedByAdmin, + PasswordReset, // device management DeviceAdded, DeviceRemoved, @@ -50,6 +53,7 @@ pub enum EventType { NetworkDeviceAdded, NetworkDeviceRemoved, NetworkDeviceModified, + ClientConfigurationTokenAdded, // audit stream AuditStreamCreated, AuditStreamModified, @@ -58,6 +62,10 @@ pub enum EventType { OpenIdAppAdded, OpenIdAppRemoved, OpenIdAppModified, + OpenIdAppStateChanged, + // OpenID provider management + OpenIdProviderRemoved, + OpenIdProviderModified, // VPN location management VpnLocationAdded, VpnLocationRemoved, @@ -69,12 +77,37 @@ pub enum EventType { VpnClientDisconnectedMfa, VpnClientMfaFailed, // Enrollment events + EnrollmentTokenAdded, EnrollmentStarted, EnrollmentDeviceAdded, EnrollmentCompleted, PasswordResetRequested, PasswordResetStarted, PasswordResetCompleted, + // API token management, + ApiTokenAdded, + ApiTokenRemoved, + ApiTokenRenamed, + // Settings management + SettingsUpdated, + SettingsUpdatedPartial, + SettingsDefaultBrandingRestored, + // Groups management + GroupsBulkAssigned, + GroupAdded, + GroupModified, + GroupRemoved, + GroupMemberAdded, + GroupMemberRemoved, + // WebHook management + WebHookAdded, + WebHookModified, + WebHookRemoved, + WebHookStateChanged, + // Authentication key management + AuthenticationKeyAdded, + AuthenticationKeyRemoved, + AuthenticationKeyRenamed, } #[derive(Model, FromRow, Serialize)] diff --git a/crates/defguard_core/src/db/models/authentication_key.rs b/crates/defguard_core/src/db/models/authentication_key.rs index cf877ca70..260ab50fe 100644 --- a/crates/defguard_core/src/db/models/authentication_key.rs +++ b/crates/defguard_core/src/db/models/authentication_key.rs @@ -13,14 +13,14 @@ pub enum AuthenticationKeyType { #[derive(Deserialize, Model, Serialize)] #[table(authentication_key)] -pub(crate) struct AuthenticationKey { - id: I, - pub yubikey_id: Option, - pub name: Option, - pub user_id: Id, - pub key: String, +pub struct AuthenticationKey { + pub(crate) id: I, + pub(crate) yubikey_id: Option, + pub(crate) name: Option, + pub(crate) user_id: Id, + pub(crate) key: String, #[model(enum)] - key_type: AuthenticationKeyType, + pub(crate) key_type: AuthenticationKeyType, } impl AuthenticationKey { diff --git a/crates/defguard_core/src/db/models/group.rs b/crates/defguard_core/src/db/models/group.rs index 4e03336c2..634929fcd 100644 --- a/crates/defguard_core/src/db/models/group.rs +++ b/crates/defguard_core/src/db/models/group.rs @@ -19,7 +19,7 @@ impl fmt::Display for Permission { } } -#[derive(Clone, Debug, Model, ToSchema, FromRow, PartialEq)] +#[derive(Clone, Debug, Model, ToSchema, FromRow, PartialEq, Serialize)] pub struct Group { pub(crate) id: I, pub name: String, diff --git a/crates/defguard_core/src/db/models/oauth2client.rs b/crates/defguard_core/src/db/models/oauth2client.rs index 91d40f9a2..41c960178 100644 --- a/crates/defguard_core/src/db/models/oauth2client.rs +++ b/crates/defguard_core/src/db/models/oauth2client.rs @@ -7,7 +7,7 @@ use crate::{ random::gen_alphanumeric, }; -#[derive(Deserialize, Model, Serialize)] +#[derive(Clone, Deserialize, Model, Serialize)] pub struct OAuth2Client { pub id: I, pub client_id: String, // unique diff --git a/crates/defguard_core/src/db/models/webauthn.rs b/crates/defguard_core/src/db/models/webauthn.rs index 29fbe4695..47aad4dc1 100644 --- a/crates/defguard_core/src/db/models/webauthn.rs +++ b/crates/defguard_core/src/db/models/webauthn.rs @@ -5,11 +5,11 @@ use webauthn_rs::prelude::Passkey; use super::error::ModelError; use crate::db::{Id, NoId}; -#[derive(Model)] +#[derive(Model, Clone)] pub struct WebAuthn { - id: I, + pub(crate) id: I, pub(crate) user_id: Id, - name: String, + pub(crate) name: String, // serialize from/to [`Passkey`] pub passkey: Vec, } diff --git a/crates/defguard_core/src/db/models/webhook.rs b/crates/defguard_core/src/db/models/webhook.rs index c9eda286c..5263c6980 100644 --- a/crates/defguard_core/src/db/models/webhook.rs +++ b/crates/defguard_core/src/db/models/webhook.rs @@ -46,7 +46,7 @@ impl AppEvent { } } -#[derive(Debug, Deserialize, FromRow, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, FromRow, Model, Serialize)] pub struct WebHook { pub id: I, pub url: String, diff --git a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs index 8bb6d879b..262c2361c 100644 --- a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs @@ -4,10 +4,10 @@ use sqlx::{query_as, Error as SqlxError, PgExecutor}; use crate::db::{Id, NoId}; -#[derive(Deserialize, Model, Serialize)] +#[derive(Clone, Deserialize, Model, Serialize)] #[table(api_token)] pub struct ApiToken { - id: I, + pub id: I, pub user_id: Id, pub created_at: NaiveDateTime, pub name: String, diff --git a/crates/defguard_core/src/enterprise/db/models/audit_stream.rs b/crates/defguard_core/src/enterprise/db/models/audit_stream.rs index ef3730ba6..1fff1822b 100644 --- a/crates/defguard_core/src/enterprise/db/models/audit_stream.rs +++ b/crates/defguard_core/src/enterprise/db/models/audit_stream.rs @@ -19,7 +19,7 @@ pub enum AuditStreamType { LogstashHttp, } -#[derive(Debug, Serialize, Model, FromRow)] +#[derive(Clone, Debug, Serialize, Model, FromRow)] #[table(audit_stream)] pub struct AuditStream { pub id: I, diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index 4039727bb..f3cb40c7e 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -85,7 +85,7 @@ impl From for DirectorySyncTarget { } } -#[derive(Deserialize, Model, Serialize)] +#[derive(Clone, Deserialize, Model, Serialize)] pub struct OpenIdProvider { pub id: I, pub name: String, @@ -199,7 +199,7 @@ impl OpenIdProvider { query_as!( OpenIdProvider, "SELECT id, name, base_url, client_id, client_secret, display_name, \ - google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, + google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", \ directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", \ directory_sync_target \"directory_sync_target: DirectorySyncTarget\", \ diff --git a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs index 2f11c46ad..808fd114b 100644 --- a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs @@ -10,8 +10,10 @@ use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, + db::User, enterprise::db::models::api_tokens::{ApiToken, ApiTokenInfo}, error::WebError, + events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{user_for_admin_or_self, ApiResponse, ApiResult}, random::gen_alphanumeric, }; @@ -28,6 +30,7 @@ pub async fn add_api_token( _admin: AdminRole, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path(username): Path, Json(data): Json, ) -> ApiResult { @@ -53,7 +56,7 @@ pub async fn add_api_token( // all API tokens start with a `dg-` prefix let token_string = format!("dg-{}", gen_alphanumeric(API_TOKEN_LENGTH)); - ApiToken::new( + let token = ApiToken::new( user.id, Utc::now().naive_utc(), data.name.clone(), @@ -63,7 +66,12 @@ pub async fn add_api_token( .await?; info!("Added new API token {} for user {username}", data.name); - + if let Some(owner) = User::find_by_id(&appstate.pool, token.user_id).await? { + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ApiTokenAdded { owner, token }), + })?; + } Ok(ApiResponse { json: json!({"token": token_string}), status: StatusCode::CREATED, @@ -96,6 +104,7 @@ pub async fn delete_api_token( _admin: AdminRole, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path((username, token_id)): Path<(String, i64)>, ) -> ApiResult { debug!("Removing API token {token_id} for user {username}"); @@ -104,7 +113,20 @@ pub async fn delete_api_token( if !session.is_admin && user.id != token.user_id { return Err(WebError::Forbidden(String::new())); } - token.delete(&appstate.pool).await?; + token.clone().delete(&appstate.pool).await?; + if let Some(owner) = User::find_by_id(&appstate.pool, token.user_id).await? { + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ApiTokenRemoved { + owner, + token: token.clone(), + }), + })?; + } + info!( + "User {} removed API token {}({token_id}) for user {username}", + user.username, token.name + ); } else { error!("API token with id {token_id} not found"); return Err(WebError::BadRequest("Key not found".into())); @@ -126,16 +148,35 @@ pub async fn rename_api_token( _admin: AdminRole, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path((username, token_id)): Path<(String, i64)>, Json(data): Json, ) -> ApiResult { + debug!("Renaming API token {token_id} for user {username}"); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; if let Some(mut token) = ApiToken::find_by_id(&appstate.pool, token_id).await? { if !session.is_admin && user.id != token.user_id { return Err(WebError::Forbidden(String::new())); } + let old_name = token.name.clone(); token.name = data.name; + let new_name = token.name.clone(); token.save(&appstate.pool).await?; + if let Some(owner) = User::find_by_id(&appstate.pool, token.user_id).await? { + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ApiTokenRenamed { + owner, + token: token.clone(), + old_name, + new_name, + }), + })?; + } + info!( + "User {} renamed API token {}({token_id}) for user {username}", + user.username, token.name + ); } else { error!("User {username} tried to rename non-existing API token with id {token_id}",); return Err(WebError::ObjectNotFound(String::new())); diff --git a/crates/defguard_core/src/enterprise/handlers/audit_stream.rs b/crates/defguard_core/src/enterprise/handlers/audit_stream.rs index 22bc66324..e920801ee 100644 --- a/crates/defguard_core/src/enterprise/handlers/audit_stream.rs +++ b/crates/defguard_core/src/enterprise/handlers/audit_stream.rs @@ -60,12 +60,8 @@ pub async fn create_audit_stream( info!("User {session_username} created audit stream"); appstate.emit_event(ApiEvent { context, - event: ApiEventType::AuditStreamCreated { - stream_id: stream.id, - stream_name: stream.name, - }, + event: Box::new(ApiEventType::AuditStreamCreated { stream }), })?; - debug!("AuditStreamCreated api event sent"); Ok(ApiResponse { json: json!({}), status: StatusCode::CREATED, @@ -84,18 +80,23 @@ pub async fn modify_audit_stream( let session_username = &session.user.username; debug!("User {session_username} modifies audit stream "); if let Some(mut stream) = AuditStream::find_by_id(&appstate.pool, id).await? { + // store stream before modifications + let before = stream.clone(); //validate config let _ = AuditStreamConfig::from_serde_value(&data.stream_type, &data.stream_config)?; stream.name = data.name; stream.config = data.stream_config; stream.save(&appstate.pool).await?; - info!("User {session_username} modified audit stream"); + info!( + "User {session_username} modified audit stream {}", + stream.name + ); appstate.emit_event(ApiEvent { context, - event: ApiEventType::AuditStreamModified { - stream_id: stream.id, - stream_name: stream.name, - }, + event: Box::new(ApiEventType::AuditStreamModified { + before, + after: stream, + }), })?; debug!("AuditStreamModified api event sent"); return Ok(ApiResponse::default()); @@ -116,15 +117,10 @@ pub async fn delete_audit_stream( let session_username = &session.user.username; debug!("User {session_username} deleting Audit stream ({id})"); if let Some(stream) = AuditStream::find_by_id(&appstate.pool, id).await? { - let stream_id = stream.id; - let stream_name = stream.name.clone(); - stream.delete(&appstate.pool).await?; + stream.clone().delete(&appstate.pool).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::AuditStreamRemoved { - stream_id, - stream_name, - }, + event: Box::new(ApiEventType::AuditStreamRemoved { stream }), })?; } else { return Err(crate::error::WebError::ObjectNotFound(format!( diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 143a210e8..92e63f292 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -17,6 +17,7 @@ use crate::{ enterprise::{ db::models::openid_provider::OpenIdProvider, directory_sync::test_directory_sync_connection, }, + events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult}, }; @@ -51,9 +52,14 @@ pub async fn add_openid_provider( _license: LicenseInfo, _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(provider_data): Json, ) -> ApiResult { + debug!( + "User {} adding OpenID provider {}", + session.user.username, provider_data.name + ); let current_provider = OpenIdProvider::get_current(&appstate.pool).await?; // The key is sent from the frontend only when user explicitly changes it, as we never send it back. @@ -148,14 +154,16 @@ pub async fn add_openid_provider( ) .upsert(&appstate.pool) .await?; - debug!( - "User {} adding OpenID provider {}", - session.user.username, new_provider.name - ); info!( "User {} added OpenID client {}", session.user.username, new_provider.name ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdProviderModified { + provider: new_provider, + }), + })?; Ok(ApiResponse { json: json!({}), @@ -197,6 +205,7 @@ pub async fn delete_openid_provider( _license: LicenseInfo, _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(provider_data): Path, ) -> ApiResult { @@ -206,55 +215,22 @@ pub async fn delete_openid_provider( ); let provider = OpenIdProvider::find_by_name(&appstate.pool, &provider_data.name).await?; if let Some(provider) = provider { - provider.delete(&appstate.pool).await?; + provider.clone().delete(&appstate.pool).await?; info!( "User {} deleted OpenID provider {}", - session.user.username, provider_data.name - ); - Ok(ApiResponse { - json: json!({}), - status: StatusCode::OK, - }) - } else { - warn!( - "User {} failed to delete OpenID provider {}. Such provider does not exist.", - session.user.username, provider_data.name - ); - Ok(ApiResponse { - json: json!({}), - status: StatusCode::NOT_FOUND, - }) - } -} - -pub async fn modify_openid_provider( - _license: LicenseInfo, - _admin: AdminRole, - session: SessionInfo, - State(appstate): State, - Json(provider_data): Json, -) -> ApiResult { - debug!( - "User {} modifying OpenID provider {}", - session.user.username, provider_data.name - ); - let provider = OpenIdProvider::find_by_name(&appstate.pool, &provider_data.name).await?; - if let Some(mut provider) = provider { - provider.base_url = provider_data.base_url; - provider.client_id = provider_data.client_id; - provider.client_secret = provider_data.client_secret; - provider.save(&appstate.pool).await?; - info!( - "User {} modified OpenID client {}", session.user.username, provider.name ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdProviderRemoved { provider }), + })?; Ok(ApiResponse { json: json!({}), status: StatusCode::OK, }) } else { warn!( - "User {} failed to modify OpenID client {}. Such client does not exist.", + "User {} failed to delete OpenID provider {}. Such provider does not exist.", session.user.username, provider_data.name ); Ok(ApiResponse { diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index d73d95e78..8d557f7f0 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -1,6 +1,14 @@ use std::net::IpAddr; -use crate::db::{Device, Id, MFAMethod, WireguardNetwork}; +use crate::{ + db::{ + models::{authentication_key::AuthenticationKey, oauth2client::OAuth2Client}, + Device, Group, Id, MFAMethod, User, WebAuthn, WebHook, WireguardNetwork, + }, + enterprise::db::models::{ + api_tokens::ApiToken, audit_stream::AuditStream, openid_provider::OpenIdProvider, + }, +}; use chrono::{NaiveDateTime, Utc}; /// Shared context that needs to be added to every API event @@ -63,9 +71,9 @@ impl GrpcRequestContext { } } -#[derive(Debug)] pub enum ApiEventType { UserLogin, + UserLogout, UserLoginFailed, UserMfaLogin { mfa_method: MFAMethod, @@ -74,81 +82,176 @@ pub enum ApiEventType { mfa_method: MFAMethod, }, RecoveryCodeUsed, - UserLogout, + PasswordChangedByAdmin { + user: User, + }, + PasswordChanged, + PasswordReset { + user: User, + }, MfaDisabled, MfaTotpDisabled, MfaTotpEnabled, MfaEmailDisabled, MfaEmailEnabled, MfaSecurityKeyAdded { - key_id: Id, - key_name: String, + key: WebAuthn, }, MfaSecurityKeyRemoved { - key_id: Id, - key_name: String, + key: WebAuthn, }, UserAdded { - username: String, + user: User, }, UserRemoved { - username: String, + user: User, }, UserModified { - username: String, + before: User, + after: User, }, UserDeviceAdded { - device_id: Id, - owner: String, - device_name: String, + owner: User, + device: Device, }, UserDeviceRemoved { - device_id: Id, - owner: String, - device_name: String, + owner: User, + device: Device, }, UserDeviceModified { - device_id: Id, - owner: String, - device_name: String, + owner: User, + before: Device, + after: Device, }, NetworkDeviceAdded { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + device: Device, + location: WireguardNetwork, }, NetworkDeviceRemoved { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + device: Device, + location: WireguardNetwork, }, NetworkDeviceModified { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + before: Device, + after: Device, + location: WireguardNetwork, }, AuditStreamCreated { - stream_id: Id, - stream_name: String, + stream: AuditStream, }, AuditStreamModified { - stream_id: Id, - stream_name: String, + before: AuditStream, + after: AuditStream, }, AuditStreamRemoved { - stream_id: Id, - stream_name: String, + stream: AuditStream, + }, + VpnLocationAdded { + location: WireguardNetwork, + }, + VpnLocationRemoved { + location: WireguardNetwork, + }, + VpnLocationModified { + before: WireguardNetwork, + after: WireguardNetwork, + }, + ApiTokenAdded { + owner: User, + token: ApiToken, + }, + ApiTokenRemoved { + owner: User, + token: ApiToken, + }, + ApiTokenRenamed { + owner: User, + token: ApiToken, + old_name: String, + new_name: String, + }, + OpenIdAppAdded { + app: OAuth2Client, + }, + OpenIdAppRemoved { + app: OAuth2Client, + }, + OpenIdAppModified { + before: OAuth2Client, + after: OAuth2Client, + }, + OpenIdAppStateChanged { + app: OAuth2Client, + enabled: bool, + }, + OpenIdProviderModified { + provider: OpenIdProvider, + }, + OpenIdProviderRemoved { + provider: OpenIdProvider, + }, + SettingsUpdated, + SettingsUpdatedPartial, + SettingsDefaultBrandingRestored, + GroupsBulkAssigned { + users: Vec>, + groups: Vec>, + }, + GroupAdded { + group: Group, + }, + GroupModified { + before: Group, + after: Group, + }, + GroupRemoved { + group: Group, + }, + GroupMemberAdded { + group: Group, + user: User, + }, + GroupMemberRemoved { + group: Group, + user: User, + }, + WebHookAdded { + webhook: WebHook, + }, + WebHookModified { + before: WebHook, + after: WebHook, + }, + WebHookRemoved { + webhook: WebHook, + }, + WebHookStateChanged { + webhook: WebHook, + enabled: bool, + }, + AuthenticationKeyAdded { + key: AuthenticationKey, + }, + AuthenticationKeyRemoved { + key: AuthenticationKey, + }, + AuthenticationKeyRenamed { + key: AuthenticationKey, + old_name: Option, + new_name: Option, + }, + EnrollmentTokenAdded { + user: User, + }, + ClientConfigurationTokenAdded { + user: User, }, } /// Events from Web API -#[derive(Debug)] pub struct ApiEvent { pub context: ApiRequestContext, - pub event: ApiEventType, + pub event: Box, } /// Events from gRPC server @@ -203,12 +306,11 @@ pub struct BidiStreamEvent { /// Wrapper enum for different types of events emitted by the bidi stream. /// /// Each variant represents a separate gRPC service that's part of the bi-directional communications server. -#[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum BidiStreamEventType { - Enrollment(EnrollmentEvent), - PasswordReset(PasswordResetEvent), - DesktopClientMfa(DesktopClientMfaEvent), + Enrollment(Box), + PasswordReset(Box), + DesktopClientMfa(Box), } #[derive(Debug)] diff --git a/crates/defguard_core/src/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/grpc/desktop_client_mfa.rs index f0909651c..f4d228903 100644 --- a/crates/defguard_core/src/grpc/desktop_client_mfa.rs +++ b/crates/defguard_core/src/grpc/desktop_client_mfa.rs @@ -28,7 +28,6 @@ use crate::{ const CLIENT_SESSION_TIMEOUT: u64 = 60 * 5; // 10 minutes #[derive(Debug, Error)] -#[allow(clippy::large_enum_variant)] pub enum ClientMfaServerError { #[error("gRPC event channel error: {0}")] BidiEventChannelError(#[from] SendError), @@ -251,13 +250,13 @@ impl ClientMfaServer { error!("Provided TOTP code is not valid"); self.emit_event(BidiStreamEvent { context, - event: BidiStreamEventType::DesktopClientMfa( + event: BidiStreamEventType::DesktopClientMfa(Box::new( DesktopClientMfaEvent::Failed { location: location.clone(), device: device.clone(), method: (*method).into(), }, - ), + )), })?; return Err(Status::unauthenticated("unauthorized")); } @@ -267,13 +266,13 @@ impl ClientMfaServer { error!("Provided email code is not valid"); self.emit_event(BidiStreamEvent { context, - event: BidiStreamEventType::DesktopClientMfa( + event: BidiStreamEventType::DesktopClientMfa(Box::new( DesktopClientMfaEvent::Failed { location: location.clone(), device: device.clone(), method: (*method).into(), }, - ), + )), })?; return Err(Status::unauthenticated("unauthorized")); } @@ -334,11 +333,13 @@ impl ClientMfaServer { ); self.emit_event(BidiStreamEvent { context, - event: BidiStreamEventType::DesktopClientMfa(DesktopClientMfaEvent::Connected { - location: location.clone(), - device: device.clone(), - method: (*method).into(), - }), + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Connected { + location: location.clone(), + device: device.clone(), + method: (*method).into(), + }, + )), })?; // remove login session from map diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs index c57ccc382..4b0205e14 100644 --- a/crates/defguard_core/src/grpc/enrollment.rs +++ b/crates/defguard_core/src/grpc/enrollment.rs @@ -104,7 +104,7 @@ impl EnrollmentServer { ) -> Result<(), SendError> { let event = BidiStreamEvent { context, - event: BidiStreamEventType::Enrollment(event), + event: BidiStreamEventType::Enrollment(Box::new(event)), }; self.bidi_event_tx.send(event) diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index 2717838c9..ee7d5c7cf 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -54,8 +54,8 @@ pub fn send_multiple_wireguard_events(events: Vec, wg_tx: &Sender< } } -#[derive(Debug, Error)] #[allow(clippy::large_enum_variant)] +#[derive(Debug, Error)] pub enum GatewayServerError { #[error("Failed to acquire lock on VPN client state map")] ClientStateMutexError, diff --git a/crates/defguard_core/src/grpc/password_reset.rs b/crates/defguard_core/src/grpc/password_reset.rs index 987716c24..b84792782 100644 --- a/crates/defguard_core/src/grpc/password_reset.rs +++ b/crates/defguard_core/src/grpc/password_reset.rs @@ -83,7 +83,7 @@ impl PasswordResetServer { ) -> Result<(), SendError> { let event = BidiStreamEvent { context, - event: BidiStreamEventType::PasswordReset(event), + event: BidiStreamEventType::PasswordReset(Box::new(event)), }; self.bidi_event_tx.send(event) diff --git a/crates/defguard_core/src/handlers/auth.rs b/crates/defguard_core/src/handlers/auth.rs index 413e4ffca..37c51e0ed 100644 --- a/crates/defguard_core/src/handlers/auth.rs +++ b/crates/defguard_core/src/handlers/auth.rs @@ -159,7 +159,7 @@ pub(crate) async fn authenticate( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLoginFailed, + event: Box::new(ApiEventType::UserLoginFailed), })?; return Err(WebError::Authorization(err.to_string())); } @@ -174,7 +174,7 @@ pub(crate) async fn authenticate( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLoginFailed, + event: Box::new(ApiEventType::UserLoginFailed), })?; return Err(WebError::Authorization(err.to_string())); } @@ -204,7 +204,7 @@ pub(crate) async fn authenticate( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLoginFailed, + event: Box::new(ApiEventType::UserLoginFailed), })?; return Err(WebError::Authorization(err.to_string())); } @@ -218,7 +218,7 @@ pub(crate) async fn authenticate( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLoginFailed, + event: Box::new(ApiEventType::UserLoginFailed), })?; return Err(WebError::Authorization(err.to_string())); } @@ -310,7 +310,7 @@ pub(crate) async fn authenticate( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLogin, + event: Box::new(ApiEventType::UserLogin), })?; Ok(( @@ -355,7 +355,7 @@ pub async fn logout( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserLogout, + event: Box::new(ApiEventType::UserLogout), })?; Ok((cookies, ApiResponse::default())) @@ -397,7 +397,7 @@ pub async fn mfa_disable( user.disable_mfa(&appstate.pool).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::MfaDisabled, + event: Box::new(ApiEventType::MfaDisabled), })?; info!("Disabled MFA for user {}", user.username); Ok(ApiResponse::default()) @@ -442,6 +442,7 @@ pub async fn webauthn_init( /// Finish WebAuthn registration pub async fn webauthn_finish( session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(webauth_reg): Json, ) -> ApiResult { @@ -485,8 +486,9 @@ pub async fn webauthn_finish( .await? .ok_or(WebError::WebauthnRegistration("User not found".into()))?; let recovery_codes = RecoveryCodes::new(user.get_recovery_codes(&appstate.pool).await?); - let webauthn = WebAuthn::new(session.session.user_id, webauth_reg.name, &passkey)?; - webauthn.save(&appstate.pool).await?; + let webauthn = WebAuthn::new(session.session.user_id, webauth_reg.name, &passkey)? + .save(&appstate.pool) + .await?; if user.mfa_method == MFAMethod::None { send_mfa_configured_email( Some(&session.session), @@ -499,6 +501,10 @@ pub async fn webauthn_finish( } info!("Finished Webauthn registration for user {}", user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::MfaSecurityKeyAdded { key: webauthn }), + })?; Ok(ApiResponse { json: json!(recovery_codes), @@ -561,9 +567,9 @@ pub async fn webauthn_end( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLogin { + event: Box::new(ApiEventType::UserMfaLogin { mfa_method: MFAMethod::Webauthn, - }, + }), })?; if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { @@ -608,9 +614,9 @@ pub async fn webauthn_end( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLoginFailed { + event: Box::new(ApiEventType::UserMfaLoginFailed { mfa_method: MFAMethod::Webauthn, - }, + }), })?; } } @@ -657,7 +663,7 @@ pub async fn totp_enable( info!("Enabled TOTP for user {}", user.username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::MfaTotpEnabled, + event: Box::new(ApiEventType::MfaTotpEnabled), })?; Ok(ApiResponse { json: json!(recovery_codes), @@ -681,7 +687,7 @@ pub async fn totp_disable( info!("Disabled TOTP for user {}", user.username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::MfaTotpDisabled, + event: Box::new(ApiEventType::MfaTotpDisabled), })?; Ok(ApiResponse::default()) } @@ -714,9 +720,9 @@ pub async fn totp_code( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLogin { + event: Box::new(ApiEventType::UserMfaLogin { mfa_method: MFAMethod::OneTimePassword, - }, + }), })?; if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); @@ -755,9 +761,9 @@ pub async fn totp_code( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLoginFailed { + event: Box::new(ApiEventType::UserMfaLoginFailed { mfa_method: MFAMethod::OneTimePassword, - }, + }), })?; Err(WebError::Authorization("Invalid TOTP code".into())) } @@ -813,7 +819,7 @@ pub async fn email_mfa_enable( info!("Enabled email MFA for user {}", user.username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::MfaEmailEnabled, + event: Box::new(ApiEventType::MfaEmailEnabled), })?; Ok(ApiResponse { json: json!(recovery_codes), @@ -837,7 +843,7 @@ pub async fn email_mfa_disable( info!("Disabled email MFA for user {}", user.username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::MfaEmailDisabled, + event: Box::new(ApiEventType::MfaEmailDisabled), })?; Ok(ApiResponse::default()) } @@ -889,9 +895,9 @@ pub async fn email_mfa_code( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLogin { + event: Box::new(ApiEventType::UserMfaLogin { mfa_method: MFAMethod::Email, - }, + }), })?; if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found openid session cookie."); @@ -930,9 +936,9 @@ pub async fn email_mfa_code( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::UserMfaLoginFailed { + event: Box::new(ApiEventType::UserMfaLoginFailed { mfa_method: MFAMethod::Email, - }, + }), })?; Err(WebError::Authorization("Invalid email MFA code".into())) } @@ -972,7 +978,7 @@ pub async fn recovery_code( insecure_ip, user_agent.to_string(), ), - event: ApiEventType::RecoveryCodeUsed, + event: Box::new(ApiEventType::RecoveryCodeUsed), })?; if let Some(openid_cookie) = private_cookies.get(SIGN_IN_COOKIE_NAME) { debug!("Found OpenID session cookie."); diff --git a/crates/defguard_core/src/handlers/group.rs b/crates/defguard_core/src/handlers/group.rs index d17b528e9..45be11372 100644 --- a/crates/defguard_core/src/handlers/group.rs +++ b/crates/defguard_core/src/handlers/group.rs @@ -19,6 +19,7 @@ use crate::{ ldap_update_users_state, }, error::WebError, + events::{ApiEvent, ApiEventType, ApiRequestContext}, hashset, }; @@ -66,6 +67,7 @@ pub(crate) struct BulkAssignToGroupsRequest { pub(crate) async fn bulk_assign_to_groups( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Json(data): Json, ) -> Result { debug!("Assigning groups to users."); @@ -123,6 +125,10 @@ pub(crate) async fn bulk_assign_to_groups( ldap_update_users_state(users_to_maybe_update, &appstate.pool).await; info!("Assigned {} groups to {} users.", groups.len(), users.len()); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupsBulkAssigned { users, groups }), + })?; Ok(ApiResponse { json: json!({}), @@ -307,6 +313,7 @@ pub(crate) async fn get_group( pub(crate) async fn create_group( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Json(group_info): Json, ) -> Result { debug!("Creating group {}", group_info.name); @@ -350,6 +357,10 @@ pub(crate) async fn create_group( } info!("Created group {}", group_info.name); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupAdded { group }), + })?; Ok(ApiResponse { json: json!(group_info), @@ -382,6 +393,7 @@ pub(crate) async fn create_group( pub(crate) async fn modify_group( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Path(name): Path, Json(group_info): Json, ) -> Result { @@ -391,6 +403,8 @@ pub(crate) async fn modify_group( error!(msg); return Err(WebError::ObjectNotFound(msg)); }; + // store group before modifications + let before = group.clone(); let mut add_to_ldap_groups: HashMap<&User, HashSet<&str>> = HashMap::new(); let mut remove_from_ldap_groups: HashMap<&User, HashSet<&str>> = HashMap::new(); @@ -477,6 +491,13 @@ pub(crate) async fn modify_group( ldap_update_users_state(affected_users, &appstate.pool).await; info!("Modified group {}", group.name); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupModified { + before, + after: group, + }), + })?; Ok(ApiResponse::default()) } @@ -507,6 +528,7 @@ pub(crate) async fn modify_group( pub(crate) async fn delete_group( _session: SessionInfo, State(appstate): State, + context: ApiRequestContext, Path(name): Path, ) -> Result { debug!("Deleting group {name}"); @@ -524,7 +546,7 @@ pub(crate) async fn delete_group( }); } } - group.delete(&appstate.pool).await?; + group.clone().delete(&appstate.pool).await?; ldap_delete_group(&name, &appstate.pool).await; // sync allowed devices for all locations @@ -532,6 +554,10 @@ pub(crate) async fn delete_group( WireguardNetwork::sync_all_networks(&mut conn, &appstate.wireguard_tx).await?; info!("Deleted group {name}"); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupRemoved { group }), + })?; Ok(ApiResponse::default()) } else { let msg = format!("Failed to find group {name}"); @@ -568,6 +594,7 @@ pub(crate) async fn delete_group( pub(crate) async fn add_group_member( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Path(name): Path, Json(data): Json, ) -> Result { @@ -580,6 +607,10 @@ pub(crate) async fn add_group_member( let mut conn = appstate.pool.acquire().await?; WireguardNetwork::sync_all_networks(&mut conn, &appstate.wireguard_tx).await?; info!("Added user: {} to group: {}", user.username, group.name); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupMemberAdded { group, user }), + })?; Ok(ApiResponse::default()) } else { error!("User not found {}", data.username); @@ -623,6 +654,7 @@ pub(crate) async fn add_group_member( pub(crate) async fn remove_group_member( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Path((name, username)): Path<(String, String)>, ) -> Result { if let Some(group) = Group::find_by_name(&appstate.pool, &name).await? { @@ -638,6 +670,10 @@ pub(crate) async fn remove_group_member( let mut conn = appstate.pool.acquire().await?; WireguardNetwork::sync_all_networks(&mut conn, &appstate.wireguard_tx).await?; info!("Removed user: {} from group: {}", user.username, group.name); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::GroupMemberRemoved { group, user }), + })?; Ok(ApiResponse { json: json!({}), status: StatusCode::OK, diff --git a/crates/defguard_core/src/handlers/network_devices.rs b/crates/defguard_core/src/handlers/network_devices.rs index 3405e9749..cdd3ccb42 100644 --- a/crates/defguard_core/src/handlers/network_devices.rs +++ b/crates/defguard_core/src/handlers/network_devices.rs @@ -631,27 +631,25 @@ pub(crate) async fn add_network_device( session.session.device_info.clone().as_deref(), )?; + let result = AddNetworkDeviceResult { + config, + device: NetworkDeviceInfo::from_device(device.clone(), &mut transaction).await?, + }; + + transaction.commit().await?; + info!( "User {} added a new network device {device_name}.", user.username ); appstate.emit_event(ApiEvent { context, - event: ApiEventType::NetworkDeviceAdded { - device_id: device.id, - device_name: device.name.clone(), - location_id: network.id, - location: network.name, - }, + event: Box::new(ApiEventType::NetworkDeviceAdded { + device, + location: network, + }), })?; - let result = AddNetworkDeviceResult { - config, - device: NetworkDeviceInfo::from_device(device, &mut transaction).await?, - }; - - transaction.commit().await?; - Ok(ApiResponse { json: json!(result), status: StatusCode::CREATED, @@ -681,6 +679,8 @@ pub async fn modify_network_device( error!("Failed to update device {device_id}, device not found"); WebError::ObjectNotFound(format!("Device {device_id} not found")) })?; + // store device before modifications + let before = device.clone(); let device_network = device .find_network_device_networks(&mut *transaction) .await? @@ -733,19 +733,18 @@ pub async fn modify_network_device( device_network.name ); } + let network_device_info = + NetworkDeviceInfo::from_device(device.clone(), &mut transaction).await?; + transaction.commit().await?; - let network_device_info = NetworkDeviceInfo::from_device(device, &mut transaction).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::NetworkDeviceModified { - device_id: network_device_info.id, - device_name: network_device_info.name.clone(), - location_id: device_network.id, - location: device_network.name, - }, + event: Box::new(ApiEventType::NetworkDeviceModified { + before, + after: device, + location: device_network, + }), })?; - transaction.commit().await?; - Ok(ApiResponse { json: json!(network_device_info), status: StatusCode::OK, diff --git a/crates/defguard_core/src/handlers/openid_clients.rs b/crates/defguard_core/src/handlers/openid_clients.rs index b9d2132ea..7d71e359b 100644 --- a/crates/defguard_core/src/handlers/openid_clients.rs +++ b/crates/defguard_core/src/handlers/openid_clients.rs @@ -12,23 +12,31 @@ use crate::{ oauth2client::{OAuth2Client, OAuth2ClientSafe}, NewOpenIDClient, }, + events::{ApiEvent, ApiEventType, ApiRequestContext}, }; pub async fn add_openid_client( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(data): Json, ) -> ApiResult { - let client = OAuth2Client::from_new(data).save(&appstate.pool).await?; debug!( "User {} adding OpenID client {}", - session.user.username, client.name + session.user.username, data.name ); + let client = OAuth2Client::from_new(data).save(&appstate.pool).await?; info!( "User {} added OpenID client {}", session.user.username, client.name ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdAppAdded { + app: client.clone(), + }), + })?; Ok(ApiResponse { json: json!(client), status: StatusCode::CREATED, @@ -36,9 +44,9 @@ pub async fn add_openid_client( } pub async fn list_openid_clients(_admin: AdminRole, State(appstate): State) -> ApiResult { - let openid_clients = OAuth2Client::all(&appstate.pool).await?; + let clients = OAuth2Client::all(&appstate.pool).await?; Ok(ApiResponse { - json: json!(openid_clients), + json: json!(clients), status: StatusCode::OK, }) } @@ -49,15 +57,15 @@ pub async fn get_openid_client( session: SessionInfo, ) -> ApiResult { match OAuth2Client::find_by_client_id(&appstate.pool, &client_id).await? { - Some(openid_client) => { + Some(client) => { if session.is_admin { Ok(ApiResponse { - json: json!(openid_client), + json: json!(client), status: StatusCode::OK, }) } else { Ok(ApiResponse { - json: json!(OAuth2ClientSafe::from(openid_client)), + json: json!(OAuth2ClientSafe::from(client)), status: StatusCode::OK, }) } @@ -72,6 +80,7 @@ pub async fn get_openid_client( pub async fn change_openid_client( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(client_id): Path, Json(data): Json, @@ -81,16 +90,25 @@ pub async fn change_openid_client( session.user.username ); let status = match OAuth2Client::find_by_client_id(&appstate.pool, &client_id).await? { - Some(mut openid_client) => { - openid_client.name = data.name; - openid_client.redirect_uri = data.redirect_uri; - openid_client.enabled = data.enabled; - openid_client.scope = data.scope; - openid_client.save(&appstate.pool).await?; + Some(mut client) => { + // store client before mods + let before = client.clone(); + client.name = data.name; + client.redirect_uri = data.redirect_uri; + client.enabled = data.enabled; + client.scope = data.scope; + client.save(&appstate.pool).await?; info!( "User {} updated OpenID client {client_id} ({})", - session.user.username, openid_client.name + session.user.username, client.name ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdAppModified { + before, + after: client, + }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, @@ -104,6 +122,7 @@ pub async fn change_openid_client( pub async fn change_openid_client_state( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(client_id): Path, Json(data): Json, @@ -113,13 +132,20 @@ pub async fn change_openid_client_state( session.user.username ); let status = match OAuth2Client::find_by_client_id(&appstate.pool, &client_id).await? { - Some(mut openid_client) => { - openid_client.enabled = data.enabled; - openid_client.save(&appstate.pool).await?; + Some(mut client) => { + client.enabled = data.enabled; + client.save(&appstate.pool).await?; info!( "User {} updated OpenID client {client_id} ({}) enabled state to {}", - session.user.username, openid_client.name, openid_client.enabled, + session.user.username, client.name, client.enabled, ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdAppStateChanged { + enabled: client.enabled, + app: client, + }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, @@ -133,6 +159,7 @@ pub async fn change_openid_client_state( pub async fn delete_openid_client( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(client_id): Path, ) -> ApiResult { @@ -141,12 +168,16 @@ pub async fn delete_openid_client( session.user.username ); let status = match OAuth2Client::find_by_client_id(&appstate.pool, &client_id).await? { - Some(openid_client) => { - openid_client.delete(&appstate.pool).await?; + Some(client) => { + client.clone().delete(&appstate.pool).await?; info!( "User {} deleted OpenID client {client_id}", session.user.username ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::OpenIdAppRemoved { app: client }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, diff --git a/crates/defguard_core/src/handlers/settings.rs b/crates/defguard_core/src/handlers/settings.rs index fad4ca348..9bfb1caf2 100644 --- a/crates/defguard_core/src/handlers/settings.rs +++ b/crates/defguard_core/src/handlers/settings.rs @@ -17,6 +17,7 @@ use crate::{ license::update_cached_license, }, error::WebError, + events::{ApiEvent, ApiEventType, ApiRequestContext}, AppState, }; @@ -47,6 +48,7 @@ pub async fn get_settings(_admin: AdminRole, State(appstate): State) - pub async fn update_settings( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(data): Json, ) -> ApiResult { @@ -57,6 +59,10 @@ pub async fn update_settings( update_current_settings(&appstate.pool, data).await?; info!("User {} updated settings", session.user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::SettingsUpdated), + })?; Ok(ApiResponse::default()) } @@ -84,6 +90,7 @@ pub async fn set_default_branding( State(appstate): State, Path(_id): Path, // TODO: check with front-end and remove. session: SessionInfo, + context: ApiRequestContext, ) -> ApiResult { debug!( "User {} restoring default branding settings", @@ -100,6 +107,10 @@ pub async fn set_default_branding( "User {} restored default branding settings", session.user.username ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::SettingsDefaultBrandingRestored), + })?; Ok(ApiResponse { json: json!(settings), status: StatusCode::OK, @@ -113,6 +124,7 @@ pub async fn patch_settings( _admin: AdminRole, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Json(data): Json, ) -> ApiResult { debug!( @@ -150,6 +162,10 @@ pub async fn patch_settings( update_current_settings(&appstate.pool, settings).await?; info!("Admin {} patched settings.", session.user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::SettingsUpdatedPartial), + })?; Ok(ApiResponse::default()) } diff --git a/crates/defguard_core/src/handlers/ssh_authorized_keys.rs b/crates/defguard_core/src/handlers/ssh_authorized_keys.rs index ef8075965..7916dd667 100644 --- a/crates/defguard_core/src/handlers/ssh_authorized_keys.rs +++ b/crates/defguard_core/src/handlers/ssh_authorized_keys.rs @@ -16,6 +16,7 @@ use crate::{ Group, Id, User, }, error::WebError, + events::{ApiEvent, ApiEventType, ApiRequestContext}, }; #[derive(Deserialize, Serialize)] @@ -156,6 +157,7 @@ pub struct AddAuthenticationKeyData { pub async fn add_authentication_key( State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path(username): Path, Json(data): Json, ) -> ApiResult { @@ -195,7 +197,7 @@ pub async fn add_authentication_key( return Err(WebError::BadRequest("Key already exists.".into())); } - AuthenticationKey::new( + let key = AuthenticationKey::new( user.id, trimmed_key.to_string(), Some(data.name.clone()), @@ -209,6 +211,10 @@ pub async fn add_authentication_key( "Added new key \"{}\" of type {:?} for user {username}", data.name, data.key_type ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::AuthenticationKeyAdded { key }), + })?; Ok(ApiResponse { json: json!({}), @@ -234,6 +240,7 @@ pub async fn fetch_authentication_keys( pub async fn delete_authentication_key( State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path((username, key_id)): Path<(String, i64)>, ) -> ApiResult { let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; @@ -241,7 +248,15 @@ pub async fn delete_authentication_key( if !session.is_admin && user.id != key.user_id { return Err(WebError::Forbidden(String::new())); } - key.delete(&appstate.pool).await?; + + info!( + "Removed key \"{:?}\"({}) of type {:?} for user {username}", + key.name, key.id, key.key_type + ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::AuthenticationKeyRemoved { key }), + })?; } else { error!("Key with id {} not found", key_id); return Err(WebError::BadRequest("Key not found".into())); @@ -261,6 +276,7 @@ pub struct RenameRequest { pub async fn rename_authentication_key( State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Path((username, key_id)): Path<(String, i64)>, Json(data): Json, ) -> ApiResult { @@ -280,8 +296,21 @@ pub async fn rename_authentication_key( ); return Err(WebError::Forbidden(String::new())); } + let old_name = key.name.clone(); key.name = Some(data.name); key.save(&appstate.pool).await?; + info!( + "User {} renamed key {:?}({}) of user with id {}", + user.username, key.name, key.id, key.user_id + ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::AuthenticationKeyRenamed { + old_name, + new_name: key.name.clone(), + key, + }), + })?; } else { error!( "User {} tried to rename non-existing key with id {}", diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index 86fdd5bdc..7fc510f56 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -342,9 +342,7 @@ pub async fn add_user( } appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserAdded { - username: user.username, - }, + event: Box::new(ApiEventType::UserAdded { user }), })?; Ok(ApiResponse { json: json!(&user_info), @@ -383,12 +381,13 @@ pub async fn add_user( pub async fn start_enrollment( _role: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(username): Path, Json(data): Json, ) -> ApiResult { debug!( - "User {} has started a new enrollment request.", + "User {} creating enrollment token for user {username}.", session.user.username ); @@ -435,7 +434,7 @@ pub async fn start_enrollment( debug!("Transaction committed."); info!( - "The enrollment process for {} has ended with success.", + "User {} created enrollment token for user {username}.", session.user.username ); debug!( @@ -443,6 +442,10 @@ pub async fn start_enrollment( enrollment_token, config.enrollment_url.to_string() ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::EnrollmentTokenAdded { user }), + })?; Ok(ApiResponse { json: json!({"enrollment_token": enrollment_token, "enrollment_url": config.enrollment_url.to_string()}), @@ -479,6 +482,7 @@ pub async fn start_enrollment( )] pub async fn start_remote_desktop_configuration( session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(username): Path, Json(data): Json, @@ -539,6 +543,10 @@ pub async fn start_remote_desktop_configuration( desktop_configuration_token, config.enrollment_url.to_string() ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::ClientConfigurationTokenAdded { user }), + })?; Ok(ApiResponse { json: json!({"enrollment_token": desktop_configuration_token, "enrollment_url": config.enrollment_url.to_string()}), @@ -630,6 +638,8 @@ pub async fn modify_user( ) -> ApiResult { debug!("User {} updating user {username}", session.user.username); let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; + // store user before mods + let before = user.clone(); let old_username = user.username.clone(); if let Err(err) = check_username(&user_info.username) { debug!("Username {} rejected: {err}", user_info.username); @@ -737,9 +747,10 @@ pub async fn modify_user( info!("User {} updated user {username}", session.user.username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserModified { - username: user.username, - }, + event: Box::new(ApiEventType::UserModified { + before, + after: user, + }), })?; Ok(ApiResponse::default()) } @@ -796,7 +807,8 @@ pub async fn delete_user( } else { None }; - user.delete_and_cleanup(&mut transaction, &appstate.wireguard_tx) + user.clone() + .delete_and_cleanup(&mut transaction, &appstate.wireguard_tx) .await?; appstate.trigger_action(AppEvent::UserDeleted(username.clone())); @@ -809,7 +821,7 @@ pub async fn delete_user( info!("User {} deleted user {}", session.user.username, &username); appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserRemoved { username }, + event: Box::new(ApiEventType::UserRemoved { user }), })?; Ok(ApiResponse::default()) } else { @@ -843,6 +855,7 @@ pub async fn delete_user( )] pub async fn change_self_password( session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(data): Json, ) -> ApiResult { @@ -869,6 +882,10 @@ pub async fn change_self_password( ldap_change_password(&mut user, &data.new_password, &appstate.pool).await; info!("User {} changed his password.", &user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::PasswordChanged), + })?; Ok(ApiResponse { json: json!({}), @@ -907,6 +924,7 @@ pub async fn change_self_password( pub async fn change_password( _role: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(username): Path, Json(data): Json, @@ -949,6 +967,10 @@ pub async fn change_password( "Admin {} changed password for user {username}", session.user.username ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::PasswordChangedByAdmin { user }), + })?; Ok(ApiResponse::default()) } else { debug!("Can't change password for user {username}, user not found"); @@ -989,6 +1011,7 @@ pub async fn change_password( pub async fn reset_password( _role: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(username): Path, ) -> ApiResult { @@ -1058,6 +1081,10 @@ pub async fn reset_password( "Admin {} reset password for user {username}", session.user.username ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::PasswordReset { user }), + })?; Ok(ApiResponse::default()) } else { debug!("Can't reset password for user {username}, user not found"); @@ -1095,6 +1122,7 @@ pub async fn reset_password( )] pub async fn delete_security_key( session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path((username, id)): Path<(String, i64)>, ) -> ApiResult { @@ -1105,12 +1133,16 @@ pub async fn delete_security_key( let mut user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; if let Some(webauthn) = WebAuthn::find_by_id(&appstate.pool, id).await? { if webauthn.user_id == user.id { - webauthn.delete(&appstate.pool).await?; + webauthn.clone().delete(&appstate.pool).await?; user.verify_mfa_state(&appstate.pool).await?; info!( "User {} deleted security key {id} for user {username}", session.user.username, ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::MfaSecurityKeyRemoved { key: webauthn }), + })?; Ok(ApiResponse::default()) } else { error!( diff --git a/crates/defguard_core/src/handlers/webhooks.rs b/crates/defguard_core/src/handlers/webhooks.rs index cc6513e36..d30374f39 100644 --- a/crates/defguard_core/src/handlers/webhooks.rs +++ b/crates/defguard_core/src/handlers/webhooks.rs @@ -9,11 +9,13 @@ use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, db::WebHook, + events::{ApiEvent, ApiEventType, ApiRequestContext}, }; pub async fn add_webhook( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Json(webhookdata): Json, ) -> ApiResult { @@ -21,10 +23,16 @@ pub async fn add_webhook( debug!("User {} adding webhook {url}", session.user.username); let webhook: WebHook = webhookdata.into(); let status = match webhook.save(&appstate.pool).await { - Ok(_) => StatusCode::CREATED, + Ok(webhook) => { + info!("User {} added webhook {url}", session.user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::WebHookAdded { webhook }), + })?; + StatusCode::CREATED + } Err(_) => StatusCode::BAD_REQUEST, }; - info!("User {} added webhook {url}", session.user.username); Ok(ApiResponse { json: json!({}), @@ -62,6 +70,7 @@ pub async fn get_webhook( pub async fn change_webhook( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(id): Path, Json(data): Json, @@ -69,6 +78,8 @@ pub async fn change_webhook( debug!("User {} updating webhook {id}", session.user.username); let status = match WebHook::find_by_id(&appstate.pool, id).await? { Some(mut webhook) => { + // store webhook before modifications + let before = webhook.clone(); webhook.url = data.url; webhook.description = data.description; webhook.token = data.token; @@ -78,11 +89,18 @@ pub async fn change_webhook( webhook.on_user_modified = data.on_user_modified; webhook.on_hwkey_provision = data.on_hwkey_provision; webhook.save(&appstate.pool).await?; + info!("User {} updated webhook {id}", session.user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::WebHookModified { + before, + after: webhook, + }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, }; - info!("User {} updated webhook {id}", session.user.username); Ok(ApiResponse { json: json!({}), @@ -93,18 +111,23 @@ pub async fn change_webhook( pub async fn delete_webhook( _admin: AdminRole, State(appstate): State, - Path(id): Path, session: SessionInfo, + context: ApiRequestContext, + Path(id): Path, ) -> ApiResult { debug!("User {} deleting webhook {id}", session.user.username); let status = match WebHook::find_by_id(&appstate.pool, id).await? { Some(webhook) => { - webhook.delete(&appstate.pool).await?; + webhook.clone().delete(&appstate.pool).await?; + info!("User {} deleted webhook {id}", session.user.username); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::WebHookRemoved { webhook }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, }; - info!("User {} deleted webhook {id}", session.user.username); Ok(ApiResponse { json: json!({}), status, @@ -119,6 +142,7 @@ pub struct ChangeStateData { pub async fn change_enabled( _admin: AdminRole, session: SessionInfo, + context: ApiRequestContext, State(appstate): State, Path(id): Path, Json(data): Json, @@ -131,14 +155,21 @@ pub async fn change_enabled( Some(mut webhook) => { webhook.enabled = data.enabled; webhook.save(&appstate.pool).await?; + info!( + "User {} changed webhook {id} enabled state to {}", + session.user.username, data.enabled + ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::WebHookStateChanged { + enabled: webhook.enabled, + webhook, + }), + })?; StatusCode::OK } None => StatusCode::NOT_FOUND, }; - info!( - "User {} changed webhook {id} enabled state to {}", - session.user.username, data.enabled - ); Ok(ApiResponse { json: json!({}), status, diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 761008376..95876b4cf 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -129,6 +129,7 @@ pub(crate) async fn create_network( _role: AdminRole, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Json(data): Json, ) -> ApiResult { let network_name = data.name.clone(); @@ -170,6 +171,13 @@ pub(crate) async fn create_network( "User {} created WireGuard network {network_name}", session.user.username ); + + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::VpnLocationAdded { + location: network.clone(), + }), + })?; update_counts(&appstate.pool).await?; Ok(ApiResponse { @@ -205,6 +213,7 @@ pub(crate) async fn modify_network( Path(network_id): Path, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, Json(data): Json, ) -> ApiResult { debug!( @@ -212,6 +221,8 @@ pub(crate) async fn modify_network( session.user.username ); let mut network = find_network(network_id, &appstate.pool).await?; + // store network before mods + let before = network.clone(); network.allowed_ips = data.parse_allowed_ips(); network.name = data.name; @@ -250,6 +261,13 @@ pub(crate) async fn modify_network( "User {} updated WireGuard network {network_id}", session.user.username, ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::VpnLocationModified { + before, + after: network.clone(), + }), + })?; Ok(ApiResponse { json: json!(network), status: StatusCode::OK, @@ -276,6 +294,7 @@ pub(crate) async fn delete_network( Path(network_id): Path, State(appstate): State, session: SessionInfo, + context: ApiRequestContext, ) -> ApiResult { debug!( "User {} deleting WireGuard network {network_id}", @@ -290,13 +309,17 @@ pub(crate) async fn delete_network( for device in network_devices { device.delete(&mut *transaction).await?; } - network.delete(&mut *transaction).await?; + network.clone().delete(&mut *transaction).await?; transaction.commit().await?; appstate.send_wireguard_event(GatewayEvent::NetworkDeleted(network_id, network_name)); info!( "User {} deleted WireGuard network {network_id}", session.user.username, ); + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::VpnLocationRemoved { location: network }), + })?; update_counts(&appstate.pool).await?; Ok(ApiResponse::default()) @@ -467,6 +490,7 @@ pub(crate) async fn remove_gateway( pub(crate) async fn import_network( _role: AdminRole, State(appstate): State, + context: ApiRequestContext, Json(data): Json, ) -> ApiResult { debug!("Importing network from config file"); @@ -507,7 +531,12 @@ pub(crate) async fn import_network( transaction.commit().await?; info!("Imported network {network} with {} devices", devices.len()); - + appstate.emit_event(ApiEvent { + context, + event: Box::new(ApiEventType::VpnLocationAdded { + location: network.clone(), + }), + })?; update_counts(&appstate.pool).await?; Ok(ApiResponse { @@ -762,21 +791,20 @@ pub(crate) async fn add_device( "User {} added device {device_name} for user {username}", session.user.username ); - // clone name to be used later - let device_name = device.name.clone(); - let device_id = device.id; - let result = AddDeviceResult { configs, device }; + let result = AddDeviceResult { + configs, + device: device.clone(), + }; update_counts(&appstate.pool).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserDeviceAdded { - device_id, - owner: username, - device_name, - }, + event: Box::new(ApiEventType::UserDeviceAdded { + device, + owner: user, + }), })?; Ok(ApiResponse { @@ -831,6 +859,8 @@ pub(crate) async fn modify_device( ) -> ApiResult { debug!("User {} updating device {device_id}", session.user.username); let mut device = device_for_admin_or_self(&appstate.pool, &session, device_id).await?; + // store device before mods + let before = device.clone(); let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { @@ -856,7 +886,6 @@ pub(crate) async fn modify_device( device.update_from(data); // clone to use later - let device_name = device.name.clone(); device.save(&appstate.pool).await?; @@ -882,14 +911,14 @@ pub(crate) async fn modify_device( info!("User {} updated device {device_id}", session.user.username); - let owner = device.get_owner(&appstate.pool).await?.username; + let owner = device.get_owner(&appstate.pool).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserDeviceModified { + event: Box::new(ApiEventType::UserDeviceModified { owner, - device_id: device.id, - device_name, - }, + before, + after: device.clone(), + }), })?; Ok(ApiResponse { @@ -984,12 +1013,8 @@ pub(crate) async fn delete_device( // prepare device info let device_info = DeviceInfo::from_device(&mut *transaction, device.clone()).await?; - // clone to use later - let device_name = device.name.clone(); - let device_type = device.device_type.clone(); - // delete device before firewall config is generated - device.delete(&mut *transaction).await?; + device.clone().delete(&mut *transaction).await?; update_counts(&mut *transaction).await?; @@ -1017,20 +1042,12 @@ pub(crate) async fn delete_device( appstate.send_multiple_wireguard_events(events); // Emit event specific to the device type. - match device_type { + match device.device_type { DeviceType::User => { - let owner = device_info - .device - .get_owner(&mut *transaction) - .await? - .username; + let owner = device_info.device.get_owner(&mut *transaction).await?; appstate.emit_event(ApiEvent { context, - event: ApiEventType::UserDeviceRemoved { - device_name, - owner, - device_id, - }, + event: Box::new(ApiEventType::UserDeviceRemoved { device, owner }), })? } DeviceType::Network => { @@ -1041,18 +1058,19 @@ pub(crate) async fn delete_device( if let Some(location) = location { appstate.emit_event(ApiEvent { context, - event: ApiEventType::NetworkDeviceRemoved { - device_id, - device_name, - location_id: location.id, - location: location.name, - }, + event: Box::new(ApiEventType::NetworkDeviceRemoved { device, location }), })?; } else { - error!("Network device {device_name}({device_id}) is assigned to non-existent location {}", network_info.network_id); + error!( + "Network device {}({}) is assigned to non-existent location {}", + device.name, device.id, network_info.network_id + ); } } else { - error!("Network device {device_name}({device_id}) has no network assigned"); + error!( + "Network device {}({}) has no network assigned", + device.name, device.id + ); } } }; diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 26b4d7ff3..500e975c0 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -10,11 +10,18 @@ use tracing::{debug, error, info, trace}; use defguard_core::db::{ models::audit_log::{ metadata::{ - AuditStreamMetadata, DeviceAddedMetadata, DeviceModifiedMetadata, - DeviceRemovedMetadata, EnrollmentDeviceAddedMetadata, MfaLoginMetadata, - MfaSecurityKeyAddedMetadata, MfaSecurityKeyRemovedMetadata, NetworkDeviceAddedMetadata, - NetworkDeviceModifiedMetadata, NetworkDeviceRemovedMetadata, UserAddedMetadata, - UserModifiedMetadata, UserRemovedMetadata, VpnClientMetadata, VpnClientMfaMetadata, + ApiTokenMetadata, ApiTokenRenamedMetadata, AuditStreamMetadata, + AuditStreamModifiedMetadata, AuthenticationKeyMetadata, + AuthenticationKeyRenamedMetadata, ClientConfigurationTokenMetadata, DeviceMetadata, + DeviceModifiedMetadata, EnrollmentDeviceAddedMetadata, EnrollmentTokenMetadata, + GroupAssignedMetadata, GroupMetadata, GroupModifiedMetadata, + GroupsBulkAssignedMetadata, MfaLoginMetadata, MfaSecurityKeyMetadata, + NetworkDeviceMetadata, NetworkDeviceModifiedMetadata, OpenIdAppMetadata, + OpenIdAppModifiedMetadata, OpenIdAppStateChangedMetadata, OpenIdProviderMetadata, + PasswordChangedByAdminMetadata, PasswordResetMetadata, UserMetadata, + UserModifiedMetadata, VpnClientMetadata, VpnClientMfaMetadata, VpnLocationMetadata, + VpnLocationModifiedMetadata, WebHookMetadata, WebHookModifiedMetadata, + WebHookStateChangedMetadata, }, AuditEvent, AuditModule, EventType, }, @@ -67,7 +74,7 @@ pub async fn run_event_logger( LoggerEvent::Defguard(event) => { let module = AuditModule::Defguard; - let (event_type, metadata) = match event { + let (event_type, metadata) = match *event { DefguardEvent::UserLogin => (EventType::UserLogin, None), DefguardEvent::UserLoginFailed => (EventType::UserLoginFailed, None), DefguardEvent::UserMfaLogin { mfa_method } => ( @@ -79,229 +86,312 @@ pub async fn run_event_logger( serde_json::to_value(MfaLoginMetadata { mfa_method }).ok(), ), DefguardEvent::UserLogout => (EventType::UserLogout, None), - DefguardEvent::UserDeviceAdded { - device_id: _, - device_name, - owner: _, - } => ( + DefguardEvent::UserDeviceAdded { owner, device } => ( EventType::DeviceAdded, - serde_json::to_value(DeviceAddedMetadata { - device_names: vec![device_name], + serde_json::to_value(DeviceMetadata { + owner: owner.into(), + device, }) .ok(), ), - DefguardEvent::UserDeviceRemoved { - device_id: _, - device_name, - owner: _, - } => ( + DefguardEvent::UserDeviceRemoved { owner, device } => ( EventType::DeviceRemoved, - serde_json::to_value(DeviceRemovedMetadata { - device_names: vec![device_name], + serde_json::to_value(DeviceMetadata { + owner: owner.into(), + device, }) .ok(), ), DefguardEvent::UserDeviceModified { - device_id: _, - device_name, - owner: _, + owner, + before, + after, } => ( EventType::DeviceModified, serde_json::to_value(DeviceModifiedMetadata { - device_names: vec![device_name], + owner: owner.into(), + before, + after, }) .ok(), ), DefguardEvent::RecoveryCodeUsed => (EventType::RecoveryCodeUsed, None), - DefguardEvent::PasswordChanged => todo!(), + DefguardEvent::PasswordChanged => (EventType::PasswordChanged, None), + DefguardEvent::PasswordChangedByAdmin { user } => ( + EventType::PasswordChangedByAdmin, + serde_json::to_value(PasswordChangedByAdminMetadata { + user: user.into(), + }) + .ok(), + ), DefguardEvent::MfaDisabled => (EventType::MfaDisabled, None), DefguardEvent::MfaTotpEnabled => (EventType::MfaTotpEnabled, None), DefguardEvent::MfaTotpDisabled => (EventType::MfaTotpDisabled, None), DefguardEvent::MfaEmailEnabled => (EventType::MfaEmailEnabled, None), DefguardEvent::MfaEmailDisabled => (EventType::MfaEmailDisabled, None), - DefguardEvent::MfaSecurityKeyAdded { key_id, key_name } => ( + DefguardEvent::MfaSecurityKeyAdded { key } => ( EventType::MfaSecurityKeyAdded, - serde_json::to_value(MfaSecurityKeyAddedMetadata { - key_id, - key_name, + serde_json::to_value(MfaSecurityKeyMetadata { key: key.into() }) + .ok(), + ), + DefguardEvent::MfaSecurityKeyRemoved { key } => ( + EventType::MfaSecurityKeyRemoved, + serde_json::to_value(MfaSecurityKeyMetadata { key: key.into() }) + .ok(), + ), + DefguardEvent::AuthenticationKeyAdded { key } => ( + EventType::AuthenticationKeyAdded, + serde_json::to_value(AuthenticationKeyMetadata { key: key.into() }) + .ok(), + ), + DefguardEvent::AuthenticationKeyRemoved { key } => ( + EventType::AuthenticationKeyRemoved, + serde_json::to_value(AuthenticationKeyMetadata { key: key.into() }) + .ok(), + ), + DefguardEvent::AuthenticationKeyRenamed { + key, + old_name, + new_name, + } => ( + EventType::AuthenticationKeyRenamed, + serde_json::to_value(AuthenticationKeyRenamedMetadata { + key: key.into(), + old_name, + new_name, }) .ok(), ), - DefguardEvent::MfaSecurityKeyRemoved { key_id, key_name } => ( - EventType::MfaSecurityKeyRemoved, - serde_json::to_value(MfaSecurityKeyRemovedMetadata { - key_id, - key_name, + DefguardEvent::ApiTokenAdded { owner, token } => ( + EventType::ApiTokenAdded, + serde_json::to_value(ApiTokenMetadata { + owner: owner.into(), + token: token.into(), + }) + .ok(), + ), + DefguardEvent::ApiTokenRemoved { owner, token } => ( + EventType::ApiTokenRemoved, + serde_json::to_value(ApiTokenMetadata { + owner: owner.into(), + token: token.into(), }) .ok(), ), - DefguardEvent::AuthenticationKeyAdded { - key_id: _, - key_name: _, - key_type: _, - } => todo!(), - DefguardEvent::AuthenticationKeyRemoved { - key_id: _, - key_name: _, - key_type: _, - } => todo!(), - DefguardEvent::AuthenticationKeyRenamed { - key_id: _, - key_name: _, - key_type: _, - } => todo!(), - DefguardEvent::ApiTokenAdded { - token_id: _, - token_name: _, - } => todo!(), - DefguardEvent::ApiTokenRemoved { - token_id: _, - token_name: _, - } => todo!(), DefguardEvent::ApiTokenRenamed { - token_id: _, - token_name: _, - } => todo!(), - DefguardEvent::UserAdded { username } => ( + owner, + token, + old_name, + new_name, + } => ( + EventType::ApiTokenRenamed, + serde_json::to_value(ApiTokenRenamedMetadata { + owner: owner.into(), + token: token.into(), + old_name, + new_name, + }) + .ok(), + ), + DefguardEvent::UserAdded { user } => ( EventType::UserAdded, - serde_json::to_value(UserAddedMetadata { username }).ok(), + serde_json::to_value(UserMetadata { user: user.into() }).ok(), ), - DefguardEvent::UserRemoved { username } => ( + DefguardEvent::UserRemoved { user } => ( EventType::UserRemoved, - serde_json::to_value(UserRemovedMetadata { username }).ok(), + serde_json::to_value(UserMetadata { user: user.into() }).ok(), ), - DefguardEvent::UserModified { username } => ( + DefguardEvent::UserModified { before, after } => ( EventType::UserModified, - serde_json::to_value(UserModifiedMetadata { username }).ok(), - ), - DefguardEvent::UserDisabled { username: _ } => todo!(), - DefguardEvent::NetworkDeviceAdded { - device_id, - device_name, - location_id, - location, - } => ( - EventType::NetworkDeviceAdded, - serde_json::to_value(NetworkDeviceAddedMetadata { - device_id, - device_name, - location_id, - location, + serde_json::to_value(UserModifiedMetadata { + before: before.into(), + after: after.into(), }) .ok(), ), - DefguardEvent::NetworkDeviceRemoved { - device_id, - device_name, - location_id, - location, - } => ( + DefguardEvent::NetworkDeviceAdded { device, location } => ( + EventType::NetworkDeviceAdded, + serde_json::to_value(NetworkDeviceMetadata { device, location }) + .ok(), + ), + DefguardEvent::NetworkDeviceRemoved { device, location } => ( EventType::NetworkDeviceRemoved, - serde_json::to_value(NetworkDeviceRemovedMetadata { - device_id, - device_name, - location_id, - location, - }) - .ok(), + serde_json::to_value(NetworkDeviceMetadata { device, location }) + .ok(), ), DefguardEvent::NetworkDeviceModified { - device_id, - device_name, - location_id, location, + before, + after, } => ( EventType::NetworkDeviceModified, serde_json::to_value(NetworkDeviceModifiedMetadata { - device_id, - device_name, - location_id, + before, + after, location, }) .ok(), ), - DefguardEvent::VpnLocationAdded { - location_id: _, - location_name: _, - } => todo!(), - DefguardEvent::VpnLocationRemoved { - location_id: _, - location_name: _, - } => todo!(), - DefguardEvent::VpnLocationModified { - location_id: _, - location_name: _, - } => todo!(), - DefguardEvent::OpenIdAppAdded { - app_id: _, - app_name: _, - } => todo!(), - DefguardEvent::OpenIdAppRemoved { - app_id: _, - app_name: _, - } => todo!(), - DefguardEvent::OpenIdAppModified { - app_id: _, - app_name: _, - } => todo!(), - DefguardEvent::OpenIdAppDisabled { - app_id: _, - app_name: _, - } => todo!(), - DefguardEvent::OpenIdProviderAdded { - provider_id: _, - provider_name: _, - } => todo!(), - DefguardEvent::OpenIdProviderRemoved { - provider_id: _, - provider_name: _, - } => todo!(), - DefguardEvent::SettingsUpdated => todo!(), - DefguardEvent::SettingsUpdatedPartial => todo!(), - DefguardEvent::SettingsDefaultBrandingRestored => todo!(), - DefguardEvent::AuditStreamCreated { - stream_id, - stream_name, - } => ( + DefguardEvent::VpnLocationAdded { location } => ( + EventType::VpnLocationAdded, + serde_json::to_value(VpnLocationMetadata { location }).ok(), + ), + DefguardEvent::VpnLocationRemoved { location } => ( + EventType::VpnLocationRemoved, + serde_json::to_value(VpnLocationMetadata { location }).ok(), + ), + DefguardEvent::VpnLocationModified { before, after } => ( + EventType::VpnLocationModified, + serde_json::to_value(VpnLocationModifiedMetadata { before, after }) + .ok(), + ), + DefguardEvent::OpenIdAppAdded { app } => ( + EventType::OpenIdAppAdded, + serde_json::to_value(OpenIdAppMetadata { app: app.into() }).ok(), + ), + DefguardEvent::OpenIdAppRemoved { app } => ( + EventType::OpenIdAppRemoved, + serde_json::to_value(OpenIdAppMetadata { app: app.into() }).ok(), + ), + DefguardEvent::OpenIdAppModified { before, after } => ( + EventType::OpenIdAppModified, + serde_json::to_value(OpenIdAppModifiedMetadata { + before: before.into(), + after: after.into(), + }) + .ok(), + ), + DefguardEvent::OpenIdAppStateChanged { app, enabled } => ( + EventType::OpenIdAppStateChanged, + serde_json::to_value(OpenIdAppStateChangedMetadata { + app: app.into(), + enabled, + }) + .ok(), + ), + DefguardEvent::OpenIdProviderModified { provider } => ( + EventType::OpenIdProviderModified, + serde_json::to_value(OpenIdProviderMetadata { + provider: provider.into(), + }) + .ok(), + ), + DefguardEvent::OpenIdProviderRemoved { provider } => ( + EventType::OpenIdProviderRemoved, + serde_json::to_value(OpenIdProviderMetadata { + provider: provider.into(), + }) + .ok(), + ), + DefguardEvent::SettingsUpdated => (EventType::SettingsUpdated, None), + DefguardEvent::SettingsUpdatedPartial => { + (EventType::SettingsUpdatedPartial, None) + } + DefguardEvent::SettingsDefaultBrandingRestored => { + (EventType::SettingsDefaultBrandingRestored, None) + } + DefguardEvent::AuditStreamCreated { stream } => ( EventType::AuditStreamCreated, serde_json::to_value(AuditStreamMetadata { - id: stream_id, - name: stream_name, + stream: stream.into(), }) .ok(), ), - DefguardEvent::AuditStreamRemoved { - stream_id, - stream_name, - } => ( + DefguardEvent::AuditStreamRemoved { stream } => ( EventType::AuditStreamRemoved, serde_json::to_value(AuditStreamMetadata { - id: stream_id, - name: stream_name, + stream: stream.into(), }) .ok(), ), - DefguardEvent::AuditStreamModified { - stream_id, - stream_name, - } => ( + DefguardEvent::AuditStreamModified { before, after } => ( EventType::AuditStreamModified, - serde_json::to_value(AuditStreamMetadata { - id: stream_id, - name: stream_name, + serde_json::to_value(AuditStreamModifiedMetadata { + before: before.into(), + after: after.into(), }) .ok(), ), + DefguardEvent::GroupsBulkAssigned { users, groups } => ( + EventType::GroupsBulkAssigned, + serde_json::to_value(GroupsBulkAssignedMetadata { + users: users.into_iter().map(Into::into).collect(), + groups, + }) + .ok(), + ), + DefguardEvent::GroupAdded { group } => ( + EventType::GroupAdded, + serde_json::to_value(GroupMetadata { group }).ok(), + ), + DefguardEvent::GroupModified { before, after } => ( + EventType::GroupModified, + serde_json::to_value(GroupModifiedMetadata { before, after }).ok(), + ), + DefguardEvent::GroupRemoved { group } => ( + EventType::GroupRemoved, + serde_json::to_value(GroupMetadata { group }).ok(), + ), + DefguardEvent::GroupMemberAdded { group, user } => ( + EventType::GroupMemberAdded, + serde_json::to_value(GroupAssignedMetadata { + group, + user: user.into(), + }) + .ok(), + ), + DefguardEvent::GroupMemberRemoved { group, user } => ( + EventType::GroupMemberRemoved, + serde_json::to_value(GroupAssignedMetadata { + group, + user: user.into(), + }) + .ok(), + ), + DefguardEvent::WebHookAdded { webhook } => ( + EventType::WebHookAdded, + serde_json::to_value(WebHookMetadata { webhook }).ok(), + ), + DefguardEvent::WebHookModified { before, after } => ( + EventType::WebHookModified, + serde_json::to_value(WebHookModifiedMetadata { before, after }) + .ok(), + ), + DefguardEvent::WebHookRemoved { webhook } => ( + EventType::WebHookRemoved, + serde_json::to_value(WebHookMetadata { webhook }).ok(), + ), + DefguardEvent::WebHookStateChanged { webhook, enabled } => ( + EventType::WebHookStateChanged, + serde_json::to_value(WebHookStateChangedMetadata { + webhook, + enabled, + }) + .ok(), + ), + DefguardEvent::PasswordReset { user } => ( + EventType::PasswordReset, + serde_json::to_value(PasswordResetMetadata { user: user.into() }) + .ok(), + ), + DefguardEvent::ClientConfigurationTokenAdded { user } => ( + EventType::ClientConfigurationTokenAdded, + serde_json::to_value(ClientConfigurationTokenMetadata { + user: user.into(), + }) + .ok(), + ), + DefguardEvent::EnrollmentTokenAdded { user } => ( + EventType::EnrollmentTokenAdded, + serde_json::to_value(EnrollmentTokenMetadata { user: user.into() }) + .ok(), + ), }; (module, event_type, metadata) } - LoggerEvent::Client(_event) => { - let _module = AuditModule::Client; - unimplemented!() - } LoggerEvent::Vpn(event) => { let module = AuditModule::Vpn; - let (event_type, metadata) = match event { + let (event_type, metadata) = match *event { VpnEvent::MfaFailed { location, device, @@ -345,7 +435,7 @@ pub async fn run_event_logger( } LoggerEvent::Enrollment(event) => { let module = AuditModule::Enrollment; - let (event_type, metadata) = match event { + let (event_type, metadata) = match *event { EnrollmentEvent::EnrollmentStarted => { (EventType::EnrollmentStarted, None) } @@ -365,6 +455,11 @@ pub async fn run_event_logger( EnrollmentEvent::PasswordResetCompleted => { (EventType::PasswordResetCompleted, None) } + EnrollmentEvent::TokenAdded { user } => ( + EventType::EnrollmentTokenAdded, + serde_json::to_value(EnrollmentTokenMetadata { user: user.into() }) + .ok(), + ), }; (module, event_type, metadata) } diff --git a/crates/defguard_event_logger/src/message.rs b/crates/defguard_event_logger/src/message.rs index 06d2b178a..476e5c0ae 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -3,7 +3,11 @@ use std::net::IpAddr; use defguard_core::{ db::{ - models::authentication_key::AuthenticationKeyType, Device, Id, MFAMethod, WireguardNetwork, + models::{authentication_key::AuthenticationKey, oauth2client::OAuth2Client}, + Device, Group, Id, MFAMethod, User, WebAuthn, WebHook, WireguardNetwork, + }, + enterprise::db::models::{ + api_tokens::ApiToken, audit_stream::AuditStream, openid_provider::OpenIdProvider, }, events::{ApiRequestContext, BidiRequestContext, GrpcRequestContext, InternalEventContext}, }; @@ -21,13 +25,10 @@ impl EventLoggerMessage { } /// Possible audit event types split by module -// TODO: remove lint override below once all events are updated to pass whole objects -#[allow(clippy::large_enum_variant)] pub enum LoggerEvent { - Defguard(DefguardEvent), - Client(ClientEvent), - Vpn(VpnEvent), - Enrollment(EnrollmentEvent), + Defguard(Box), + Vpn(Box), + Enrollment(Box), } /// Shared context that's included in all events @@ -89,8 +90,8 @@ impl From for EventContext { /// Represents audit events related to actions performed in Web UI pub enum DefguardEvent { - // authentication UserLogin, + UserLogout, UserLoginFailed, UserMfaLogin { mfa_method: MFAMethod, @@ -98,161 +99,171 @@ pub enum DefguardEvent { UserMfaLoginFailed { mfa_method: MFAMethod, }, - UserLogout, RecoveryCodeUsed, + PasswordChangedByAdmin { + user: User, + }, PasswordChanged, - // user MFA management + PasswordReset { + user: User, + }, MfaDisabled, - MfaTotpEnabled, MfaTotpDisabled, - MfaEmailEnabled, + MfaTotpEnabled, MfaEmailDisabled, + MfaEmailEnabled, MfaSecurityKeyAdded { - key_id: Id, - key_name: String, + key: WebAuthn, }, MfaSecurityKeyRemoved { - key_id: Id, - key_name: String, - }, - // authentication key management - AuthenticationKeyAdded { - key_id: Id, - key_name: String, - key_type: AuthenticationKeyType, - }, - AuthenticationKeyRemoved { - key_id: Id, - key_name: String, - key_type: AuthenticationKeyType, - }, - AuthenticationKeyRenamed { - key_id: Id, - key_name: String, - key_type: AuthenticationKeyType, - }, - // API token management - ApiTokenAdded { - token_id: Id, - token_name: String, - }, - ApiTokenRemoved { - token_id: Id, - token_name: String, - }, - ApiTokenRenamed { - token_id: Id, - token_name: String, + key: WebAuthn, }, - // user management UserAdded { - username: String, + user: User, }, UserRemoved { - username: String, + user: User, }, UserModified { - username: String, - }, - UserDisabled { - username: String, + before: User, + after: User, }, - // device management UserDeviceAdded { - device_id: Id, - device_name: String, - owner: String, + owner: User, + device: Device, }, UserDeviceRemoved { - device_id: Id, - device_name: String, - owner: String, + owner: User, + device: Device, }, UserDeviceModified { - device_id: Id, - device_name: String, - owner: String, + owner: User, + before: Device, + after: Device, }, NetworkDeviceAdded { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + device: Device, + location: WireguardNetwork, }, NetworkDeviceRemoved { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + device: Device, + location: WireguardNetwork, }, NetworkDeviceModified { - device_id: Id, - device_name: String, - location_id: Id, - location: String, + before: Device, + after: Device, + location: WireguardNetwork, + }, + AuditStreamCreated { + stream: AuditStream, + }, + AuditStreamModified { + before: AuditStream, + after: AuditStream, + }, + AuditStreamRemoved { + stream: AuditStream, }, - // VPN location management VpnLocationAdded { - location_id: Id, - location_name: String, + location: WireguardNetwork, }, VpnLocationRemoved { - location_id: Id, - location_name: String, + location: WireguardNetwork, }, VpnLocationModified { - location_id: Id, - location_name: String, + before: WireguardNetwork, + after: WireguardNetwork, + }, + ApiTokenAdded { + owner: User, + token: ApiToken, + }, + ApiTokenRemoved { + owner: User, + token: ApiToken, + }, + ApiTokenRenamed { + owner: User, + token: ApiToken, + old_name: String, + new_name: String, }, - // OpenID app management OpenIdAppAdded { - app_id: Id, - app_name: String, + app: OAuth2Client, }, OpenIdAppRemoved { - app_id: Id, - app_name: String, + app: OAuth2Client, }, OpenIdAppModified { - app_id: Id, - app_name: String, + before: OAuth2Client, + after: OAuth2Client, }, - OpenIdAppDisabled { - app_id: Id, - app_name: String, + OpenIdAppStateChanged { + app: OAuth2Client, + enabled: bool, }, - // OpenID provider management - OpenIdProviderAdded { - provider_id: Id, - provider_name: String, + OpenIdProviderModified { + provider: OpenIdProvider, }, OpenIdProviderRemoved { - provider_id: Id, - provider_name: String, + provider: OpenIdProvider, }, - // settings management SettingsUpdated, SettingsUpdatedPartial, SettingsDefaultBrandingRestored, - // audit stream management - AuditStreamCreated { - stream_id: Id, - stream_name: String, + GroupsBulkAssigned { + users: Vec>, + groups: Vec>, }, - AuditStreamModified { - stream_id: Id, - stream_name: String, + GroupAdded { + group: Group, }, - AuditStreamRemoved { - stream_id: Id, - stream_name: String, + GroupModified { + before: Group, + after: Group, + }, + GroupRemoved { + group: Group, + }, + GroupMemberAdded { + group: Group, + user: User, + }, + GroupMemberRemoved { + group: Group, + user: User, + }, + WebHookAdded { + webhook: WebHook, + }, + WebHookModified { + before: WebHook, + after: WebHook, + }, + WebHookRemoved { + webhook: WebHook, + }, + WebHookStateChanged { + webhook: WebHook, + enabled: bool, + }, + AuthenticationKeyAdded { + key: AuthenticationKey, + }, + AuthenticationKeyRemoved { + key: AuthenticationKey, + }, + AuthenticationKeyRenamed { + key: AuthenticationKey, + old_name: Option, + new_name: Option, + }, + EnrollmentTokenAdded { + user: User, + }, + ClientConfigurationTokenAdded { + user: User, }, -} - -/// Represents audit events related to client applications -pub enum ClientEvent { - DesktopClientActivated { device_id: Id, device_name: String }, - DesktopClientUpdated { device_id: Id, device_name: String }, } /// Represents audit events related to VPN @@ -289,4 +300,5 @@ pub enum EnrollmentEvent { PasswordResetRequested, PasswordResetStarted, PasswordResetCompleted, + TokenAdded { user: User }, } diff --git a/crates/defguard_event_router/src/events.rs b/crates/defguard_event_router/src/events.rs index 02c0fb10b..ca02c1a57 100644 --- a/crates/defguard_event_router/src/events.rs +++ b/crates/defguard_event_router/src/events.rs @@ -4,9 +4,6 @@ use defguard_core::events::{ApiEvent, BidiStreamEvent, GrpcEvent, InternalEvent} /// /// System components can send events to the event router through their own event channels. /// The enum itself is organized based on event source to make splitting logic into smaller chunks easier. -// TODO: remove lint override below once all events are updated to pass whole objects -#[allow(clippy::large_enum_variant)] -#[derive(Debug)] pub enum Event { Api(ApiEvent), Grpc(GrpcEvent), diff --git a/crates/defguard_event_router/src/handlers/api.rs b/crates/defguard_event_router/src/handlers/api.rs index 4c4c9dd4e..2d3ef2637 100644 --- a/crates/defguard_event_router/src/handlers/api.rs +++ b/crates/defguard_event_router/src/handlers/api.rs @@ -1,139 +1,237 @@ use defguard_core::events::{ApiEvent, ApiEventType}; -use defguard_event_logger::message::{DefguardEvent, LoggerEvent}; +use defguard_event_logger::message::{DefguardEvent, EnrollmentEvent, LoggerEvent}; use tracing::debug; use crate::{error::EventRouterError, EventRouter}; impl EventRouter { pub(crate) fn handle_api_event(&self, event: ApiEvent) -> Result<(), EventRouterError> { - debug!("Processing API event: {event:?}"); - let logger_event = match event.event { - ApiEventType::UserLogin => LoggerEvent::Defguard(DefguardEvent::UserLogin), - ApiEventType::UserLoginFailed => LoggerEvent::Defguard(DefguardEvent::UserLoginFailed), + debug!("Processing API event"); + let logger_event = match *event.event { + ApiEventType::UserLogin => LoggerEvent::Defguard(Box::new(DefguardEvent::UserLogin)), + ApiEventType::UserLoginFailed => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserLoginFailed)) + } ApiEventType::UserMfaLogin { mfa_method } => { - LoggerEvent::Defguard(DefguardEvent::UserMfaLogin { mfa_method }) + LoggerEvent::Defguard(Box::new(DefguardEvent::UserMfaLogin { mfa_method })) } ApiEventType::UserMfaLoginFailed { mfa_method } => { - LoggerEvent::Defguard(DefguardEvent::UserMfaLoginFailed { mfa_method }) + LoggerEvent::Defguard(Box::new(DefguardEvent::UserMfaLoginFailed { mfa_method })) } ApiEventType::RecoveryCodeUsed => { - LoggerEvent::Defguard(DefguardEvent::RecoveryCodeUsed) + LoggerEvent::Defguard(Box::new(DefguardEvent::RecoveryCodeUsed)) + } + ApiEventType::UserLogout => LoggerEvent::Defguard(Box::new(DefguardEvent::UserLogout)), + ApiEventType::UserAdded { user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserAdded { user })) + } + ApiEventType::UserRemoved { user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserRemoved { user })) + } + ApiEventType::UserModified { before, after } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserModified { before, after })) } - ApiEventType::UserLogout => LoggerEvent::Defguard(DefguardEvent::UserLogout), - ApiEventType::UserAdded { username } => { - LoggerEvent::Defguard(DefguardEvent::UserAdded { username }) + ApiEventType::MfaDisabled => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaDisabled)) } - ApiEventType::UserRemoved { username } => { - LoggerEvent::Defguard(DefguardEvent::UserRemoved { username }) + ApiEventType::MfaTotpDisabled => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaTotpDisabled)) } - ApiEventType::UserModified { username } => { - LoggerEvent::Defguard(DefguardEvent::UserModified { username }) + ApiEventType::MfaTotpEnabled => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaTotpEnabled)) } - ApiEventType::MfaDisabled => LoggerEvent::Defguard(DefguardEvent::MfaDisabled), - ApiEventType::MfaTotpDisabled => LoggerEvent::Defguard(DefguardEvent::MfaTotpDisabled), - ApiEventType::MfaTotpEnabled => LoggerEvent::Defguard(DefguardEvent::MfaTotpEnabled), ApiEventType::MfaEmailDisabled => { - LoggerEvent::Defguard(DefguardEvent::MfaEmailDisabled) + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaEmailDisabled)) } - ApiEventType::MfaEmailEnabled => LoggerEvent::Defguard(DefguardEvent::MfaEmailEnabled), - ApiEventType::MfaSecurityKeyAdded { key_id, key_name } => { - LoggerEvent::Defguard(DefguardEvent::MfaSecurityKeyAdded { key_id, key_name }) + ApiEventType::MfaEmailEnabled => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaEmailEnabled)) } - ApiEventType::MfaSecurityKeyRemoved { key_id, key_name } => { - LoggerEvent::Defguard(DefguardEvent::MfaSecurityKeyRemoved { key_id, key_name }) + ApiEventType::MfaSecurityKeyAdded { key } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaSecurityKeyAdded { key })) + } + ApiEventType::MfaSecurityKeyRemoved { key } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::MfaSecurityKeyRemoved { key })) + } + ApiEventType::UserDeviceAdded { owner, device } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserDeviceAdded { device, owner })) + } + ApiEventType::UserDeviceRemoved { owner, device } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::UserDeviceRemoved { device, owner })) } - ApiEventType::UserDeviceAdded { - owner, - device_id, - device_name, - } => LoggerEvent::Defguard(DefguardEvent::UserDeviceAdded { - device_name, - device_id, - owner, - }), - ApiEventType::UserDeviceRemoved { - owner, - device_id, - device_name, - } => LoggerEvent::Defguard(DefguardEvent::UserDeviceRemoved { - device_name, - device_id, - owner, - }), ApiEventType::UserDeviceModified { owner, - device_id, - device_name, - } => LoggerEvent::Defguard(DefguardEvent::UserDeviceModified { - device_name, - device_id, + before, + after, + } => LoggerEvent::Defguard(Box::new(DefguardEvent::UserDeviceModified { owner, - }), - ApiEventType::NetworkDeviceAdded { - device_id, - device_name, - location_id, - location, - } => LoggerEvent::Defguard(DefguardEvent::NetworkDeviceAdded { - device_id, - device_name, - location_id, - location, - }), + before, + after, + })), + ApiEventType::NetworkDeviceAdded { device, location } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::NetworkDeviceAdded { + device, + location, + })) + } ApiEventType::NetworkDeviceModified { - device_id, - device_name, - location_id, - location, - } => LoggerEvent::Defguard(DefguardEvent::NetworkDeviceModified { - device_id, - device_name, - location_id, + before, + after, location, - }), - ApiEventType::NetworkDeviceRemoved { - device_id, - device_name, - location_id, + } => LoggerEvent::Defguard(Box::new(DefguardEvent::NetworkDeviceModified { + before, + after, location, - } => LoggerEvent::Defguard(DefguardEvent::NetworkDeviceRemoved { - device_id, - device_name, - location_id, - location, - }), - ApiEventType::AuditStreamCreated { - stream_id, - stream_name, - } => { + })), + ApiEventType::NetworkDeviceRemoved { device, location } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::NetworkDeviceRemoved { + device, + location, + })) + } + ApiEventType::AuditStreamCreated { stream } => { // Notify stream manager about configuration changes self.audit_stream_reload_notify.notify_waiters(); - LoggerEvent::Defguard(DefguardEvent::AuditStreamCreated { - stream_id, - stream_name, - }) - } - ApiEventType::AuditStreamModified { - stream_id, - stream_name, - } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::AuditStreamCreated { stream })) + } + ApiEventType::AuditStreamModified { before, after } => { // Notify stream manager about configuration changes self.audit_stream_reload_notify.notify_waiters(); - LoggerEvent::Defguard(DefguardEvent::AuditStreamModified { - stream_id, - stream_name, - }) - } - ApiEventType::AuditStreamRemoved { - stream_id, - stream_name, - } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::AuditStreamModified { + before, + after, + })) + } + ApiEventType::AuditStreamRemoved { stream } => { // Notify stream manager about configuration changes self.audit_stream_reload_notify.notify_waiters(); - LoggerEvent::Defguard(DefguardEvent::AuditStreamRemoved { - stream_id, - stream_name, - }) + LoggerEvent::Defguard(Box::new(DefguardEvent::AuditStreamRemoved { stream })) + } + ApiEventType::VpnLocationAdded { location } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::VpnLocationAdded { location })) + } + ApiEventType::VpnLocationRemoved { location } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::VpnLocationRemoved { location })) + } + ApiEventType::VpnLocationModified { before, after } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::VpnLocationModified { + before, + after, + })) + } + ApiEventType::ApiTokenAdded { owner, token } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::ApiTokenAdded { owner, token })) + } + ApiEventType::ApiTokenRemoved { owner, token } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::ApiTokenRemoved { owner, token })) + } + ApiEventType::ApiTokenRenamed { + owner, + token, + old_name, + new_name, + } => LoggerEvent::Defguard(Box::new(DefguardEvent::ApiTokenRenamed { + owner, + token, + old_name, + new_name, + })), + ApiEventType::OpenIdAppAdded { app } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdAppAdded { app })) + } + ApiEventType::OpenIdAppRemoved { app } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdAppRemoved { app })) + } + ApiEventType::OpenIdAppModified { before, after } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdAppModified { before, after })) + } + ApiEventType::OpenIdAppStateChanged { app, enabled } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdAppStateChanged { + app, + enabled, + })) + } + ApiEventType::OpenIdProviderRemoved { provider } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdProviderRemoved { provider })) + } + ApiEventType::OpenIdProviderModified { provider } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::OpenIdProviderModified { provider })) + } + ApiEventType::SettingsUpdated => { + LoggerEvent::Defguard(Box::new(DefguardEvent::SettingsUpdated)) + } + ApiEventType::SettingsUpdatedPartial => { + LoggerEvent::Defguard(Box::new(DefguardEvent::SettingsUpdatedPartial)) + } + ApiEventType::SettingsDefaultBrandingRestored => { + LoggerEvent::Defguard(Box::new(DefguardEvent::SettingsDefaultBrandingRestored)) + } + ApiEventType::GroupsBulkAssigned { users, groups } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupsBulkAssigned { + users, + groups, + })) + } + ApiEventType::GroupAdded { group } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupAdded { group })) + } + ApiEventType::GroupModified { before, after } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupModified { before, after })) + } + ApiEventType::GroupRemoved { group } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupRemoved { group })) + } + ApiEventType::GroupMemberAdded { group, user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupMemberAdded { group, user })) + } + ApiEventType::GroupMemberRemoved { group, user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::GroupMemberRemoved { group, user })) + } + ApiEventType::WebHookAdded { webhook } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::WebHookAdded { webhook })) + } + ApiEventType::WebHookModified { before, after } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::WebHookModified { before, after })) + } + ApiEventType::WebHookRemoved { webhook } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::WebHookRemoved { webhook })) + } + ApiEventType::WebHookStateChanged { webhook, enabled } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::WebHookStateChanged { + webhook, + enabled, + })) + } + ApiEventType::AuthenticationKeyAdded { key } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::AuthenticationKeyAdded { key })) + } + ApiEventType::AuthenticationKeyRemoved { key } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::AuthenticationKeyRemoved { key })) + } + ApiEventType::AuthenticationKeyRenamed { + key, + old_name, + new_name, + } => LoggerEvent::Defguard(Box::new(DefguardEvent::AuthenticationKeyRenamed { + key, + old_name, + new_name, + })), + ApiEventType::EnrollmentTokenAdded { user } => { + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::TokenAdded { user })) + } + ApiEventType::PasswordChanged => { + LoggerEvent::Defguard(Box::new(DefguardEvent::PasswordChanged)) + } + ApiEventType::PasswordChangedByAdmin { user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::PasswordChangedByAdmin { user })) + } + ApiEventType::PasswordReset { user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::PasswordReset { user })) + } + ApiEventType::ClientConfigurationTokenAdded { user } => { + LoggerEvent::Defguard(Box::new(DefguardEvent::ClientConfigurationTokenAdded { + user, + })) } }; self.log_event(event.context.into(), logger_event) diff --git a/crates/defguard_event_router/src/handlers/bidi.rs b/crates/defguard_event_router/src/handlers/bidi.rs index b6a3681dc..5c00c1895 100644 --- a/crates/defguard_event_router/src/handlers/bidi.rs +++ b/crates/defguard_event_router/src/handlers/bidi.rs @@ -12,49 +12,51 @@ impl EventRouter { let BidiStreamEvent { context, event } = event; let logger_event = match event { - BidiStreamEventType::Enrollment(event) => match event { + BidiStreamEventType::Enrollment(event) => match *event { events::EnrollmentEvent::EnrollmentStarted => { - LoggerEvent::Enrollment(EnrollmentEvent::EnrollmentStarted) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::EnrollmentStarted)) } events::EnrollmentEvent::EnrollmentCompleted => { - LoggerEvent::Enrollment(EnrollmentEvent::EnrollmentCompleted) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::EnrollmentCompleted)) } events::EnrollmentEvent::EnrollmentDeviceAdded { device } => { - LoggerEvent::Enrollment(EnrollmentEvent::EnrollmentDeviceAdded { device }) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::EnrollmentDeviceAdded { + device, + })) } }, - BidiStreamEventType::PasswordReset(event) => match event { + BidiStreamEventType::PasswordReset(event) => match *event { PasswordResetEvent::PasswordResetRequested => { - LoggerEvent::Enrollment(EnrollmentEvent::PasswordResetRequested) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::PasswordResetRequested)) } PasswordResetEvent::PasswordResetStarted => { - LoggerEvent::Enrollment(EnrollmentEvent::PasswordResetStarted) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::PasswordResetStarted)) } PasswordResetEvent::PasswordResetCompleted => { - LoggerEvent::Enrollment(EnrollmentEvent::PasswordResetCompleted) + LoggerEvent::Enrollment(Box::new(EnrollmentEvent::PasswordResetCompleted)) } }, - BidiStreamEventType::DesktopClientMfa(event) => match event { + BidiStreamEventType::DesktopClientMfa(event) => match *event { DesktopClientMfaEvent::Connected { location, device, method, - } => LoggerEvent::Vpn(VpnEvent::ConnectedToMfaLocation { + } => LoggerEvent::Vpn(Box::new(VpnEvent::ConnectedToMfaLocation { location, device, method, - }), + })), DesktopClientMfaEvent::Failed { location, device, method, - } => LoggerEvent::Vpn(VpnEvent::MfaFailed { + } => LoggerEvent::Vpn(Box::new(VpnEvent::MfaFailed { location, device, method, - }), + })), }, }; diff --git a/crates/defguard_event_router/src/handlers/grpc.rs b/crates/defguard_event_router/src/handlers/grpc.rs index 282de5c99..2f4b62f93 100644 --- a/crates/defguard_event_router/src/handlers/grpc.rs +++ b/crates/defguard_event_router/src/handlers/grpc.rs @@ -18,7 +18,7 @@ impl EventRouter { } => { self.log_event( context.into(), - LoggerEvent::Vpn(VpnEvent::ConnectedToLocation { location, device }), + LoggerEvent::Vpn(Box::new(VpnEvent::ConnectedToLocation { location, device })), )?; } GrpcEvent::ClientDisconnected { @@ -28,7 +28,10 @@ impl EventRouter { } => { self.log_event( context.into(), - LoggerEvent::Vpn(VpnEvent::DisconnectedFromLocation { location, device }), + LoggerEvent::Vpn(Box::new(VpnEvent::DisconnectedFromLocation { + location, + device, + })), )?; } } diff --git a/crates/defguard_event_router/src/handlers/internal.rs b/crates/defguard_event_router/src/handlers/internal.rs index d07cfb4ba..2dee7b195 100644 --- a/crates/defguard_event_router/src/handlers/internal.rs +++ b/crates/defguard_event_router/src/handlers/internal.rs @@ -16,7 +16,10 @@ impl EventRouter { let device = context.device.clone(); self.log_event( context.into(), - LoggerEvent::Vpn(VpnEvent::DisconnectedFromMfaLocation { device, location }), + LoggerEvent::Vpn(Box::new(VpnEvent::DisconnectedFromMfaLocation { + device, + location, + })), ) } } diff --git a/crates/defguard_event_router/src/lib.rs b/crates/defguard_event_router/src/lib.rs index bd4f4e013..c2c1b73de 100644 --- a/crates/defguard_event_router/src/lib.rs +++ b/crates/defguard_event_router/src/lib.rs @@ -148,7 +148,7 @@ impl EventRouter { }, }; - debug!("Received event: {event:?}"); + debug!("Received event"); // Route the event to the appropriate handler match event { diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index c2df2e3af..9de72cd3f 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1363,7 +1363,7 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do modulesVisibility: { header: 'Modules Visibility', helper: `

- If your not using some modules you can disable their visibility. + Hide unused modules.

Read more in documentation. @@ -2575,12 +2575,44 @@ This alias is currently in use by the following rule(s) and cannot be deleted. T vpn_client_connected_mfa: 'VPN client connected to MFA location', vpn_client_disconnected_mfa: 'VPN client disconnected from MFA location', vpn_client_mfa_failed: 'VPN client failed MFA authentication', + enrollment_token_added: 'Enrollment token added', enrollment_started: 'Enrollment started', enrollment_device_added: 'Device added', enrollment_completed: 'Enrollment completed', password_reset_requested: 'Password reset requested', password_reset_started: 'Password reset started', password_reset_completed: 'Password reset completed', + vpn_location_added: 'VPN location added', + vpn_location_removed: 'VPN location removed', + vpn_location_modified: 'VPN location modified', + api_token_added: 'API token added', + api_token_removed: 'API token removed', + api_token_renamed: 'API token renamed', + open_id_app_added: 'OpenID app added', + open_id_app_removed: 'OpenID app removed', + open_id_app_modified: 'OpenID app modified', + open_id_app_state_changed: 'OpenID app state changed', + open_id_provider_removed: 'OpenID provider removed', + open_id_provider_modified: 'OpenID provider modified', + settings_updated: 'Settings updated', + settings_updated_partial: 'Settings partially updated', + settings_default_branding_restored: 'Default branding restored', + groups_bulk_assigned: 'Groups bulk assigned', + group_added: 'Group added', + group_modified: 'Group modified', + group_removed: 'Group removed', + group_member_added: 'Group member added', + group_member_removed: 'Group member removed', + web_hook_added: 'Webhook added', + web_hook_modified: 'Webhook modified', + web_hook_removed: 'Webhook removed', + authentication_key_added: 'Authentication key added', + authentication_key_removed: 'Authentication key removed', + authentication_key_renamed: 'Authentication key renamed', + password_changed: 'Password changed', + password_changed_by_admin: 'Password changed by admin', + password_reset: 'Password reset', + client_configuration_token_added: 'Client configuration token added', }, auditModule: { defguard: 'Defguard', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 8aadfa84a..fc1b5afde 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -3329,7 +3329,7 @@ type RootTranslation = { header: string /** * <​p​>​ - ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​I​f​ ​y​o​u​r​ ​n​o​t​ ​u​s​i​n​g​ ​s​o​m​e​ ​m​o​d​u​l​e​s​ ​y​o​u​ ​c​a​n​ ​d​i​s​a​b​l​e​ ​t​h​e​i​r​ ​v​i​s​i​b​i​l​i​t​y​.​ + ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​H​i​d​e​ ​u​n​u​s​e​d​ ​m​o​d​u​l​e​s​.​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​/​p​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​<​a​ ​h​r​e​f​=​"​{​d​o​c​u​m​e​n​t​a​t​i​o​n​L​i​n​k​}​"​ ​t​a​r​g​e​t​=​"​_​b​l​a​n​k​"​>​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​ ​R​e​a​d​ ​m​o​r​e​ ​i​n​ ​d​o​c​u​m​e​n​t​a​t​i​o​n​.​ @@ -6214,6 +6214,10 @@ type RootTranslation = { * V​P​N​ ​c​l​i​e​n​t​ ​f​a​i​l​e​d​ ​M​F​A​ ​a​u​t​h​e​n​t​i​c​a​t​i​o​n */ vpn_client_mfa_failed: string + /** + * E​n​r​o​l​l​m​e​n​t​ ​t​o​k​e​n​ ​a​d​d​e​d + */ + enrollment_token_added: string /** * E​n​r​o​l​l​m​e​n​t​ ​s​t​a​r​t​e​d */ @@ -6238,6 +6242,130 @@ type RootTranslation = { * P​a​s​s​w​o​r​d​ ​r​e​s​e​t​ ​c​o​m​p​l​e​t​e​d */ password_reset_completed: string + /** + * V​P​N​ ​l​o​c​a​t​i​o​n​ ​a​d​d​e​d + */ + vpn_location_added: string + /** + * V​P​N​ ​l​o​c​a​t​i​o​n​ ​r​e​m​o​v​e​d + */ + vpn_location_removed: string + /** + * V​P​N​ ​l​o​c​a​t​i​o​n​ ​m​o​d​i​f​i​e​d + */ + vpn_location_modified: string + /** + * A​P​I​ ​t​o​k​e​n​ ​a​d​d​e​d + */ + api_token_added: string + /** + * A​P​I​ ​t​o​k​e​n​ ​r​e​m​o​v​e​d + */ + api_token_removed: string + /** + * A​P​I​ ​t​o​k​e​n​ ​r​e​n​a​m​e​d + */ + api_token_renamed: string + /** + * O​p​e​n​I​D​ ​a​p​p​ ​a​d​d​e​d + */ + open_id_app_added: string + /** + * O​p​e​n​I​D​ ​a​p​p​ ​r​e​m​o​v​e​d + */ + open_id_app_removed: string + /** + * O​p​e​n​I​D​ ​a​p​p​ ​m​o​d​i​f​i​e​d + */ + open_id_app_modified: string + /** + * O​p​e​n​I​D​ ​a​p​p​ ​s​t​a​t​e​ ​c​h​a​n​g​e​d + */ + open_id_app_state_changed: string + /** + * O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​ ​r​e​m​o​v​e​d + */ + open_id_provider_removed: string + /** + * O​p​e​n​I​D​ ​p​r​o​v​i​d​e​r​ ​m​o​d​i​f​i​e​d + */ + open_id_provider_modified: string + /** + * S​e​t​t​i​n​g​s​ ​u​p​d​a​t​e​d + */ + settings_updated: string + /** + * S​e​t​t​i​n​g​s​ ​p​a​r​t​i​a​l​l​y​ ​u​p​d​a​t​e​d + */ + settings_updated_partial: string + /** + * D​e​f​a​u​l​t​ ​b​r​a​n​d​i​n​g​ ​r​e​s​t​o​r​e​d + */ + settings_default_branding_restored: string + /** + * G​r​o​u​p​s​ ​b​u​l​k​ ​a​s​s​i​g​n​e​d + */ + groups_bulk_assigned: string + /** + * G​r​o​u​p​ ​a​d​d​e​d + */ + group_added: string + /** + * G​r​o​u​p​ ​m​o​d​i​f​i​e​d + */ + group_modified: string + /** + * G​r​o​u​p​ ​r​e​m​o​v​e​d + */ + group_removed: string + /** + * G​r​o​u​p​ ​m​e​m​b​e​r​ ​a​d​d​e​d + */ + group_member_added: string + /** + * G​r​o​u​p​ ​m​e​m​b​e​r​ ​r​e​m​o​v​e​d + */ + group_member_removed: string + /** + * W​e​b​h​o​o​k​ ​a​d​d​e​d + */ + web_hook_added: string + /** + * W​e​b​h​o​o​k​ ​m​o​d​i​f​i​e​d + */ + web_hook_modified: string + /** + * W​e​b​h​o​o​k​ ​r​e​m​o​v​e​d + */ + web_hook_removed: string + /** + * A​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​k​e​y​ ​a​d​d​e​d + */ + authentication_key_added: string + /** + * A​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​k​e​y​ ​r​e​m​o​v​e​d + */ + authentication_key_removed: string + /** + * A​u​t​h​e​n​t​i​c​a​t​i​o​n​ ​k​e​y​ ​r​e​n​a​m​e​d + */ + authentication_key_renamed: string + /** + * P​a​s​s​w​o​r​d​ ​c​h​a​n​g​e​d + */ + password_changed: string + /** + * P​a​s​s​w​o​r​d​ ​c​h​a​n​g​e​d​ ​b​y​ ​a​d​m​i​n + */ + password_changed_by_admin: string + /** + * P​a​s​s​w​o​r​d​ ​r​e​s​e​t + */ + password_reset: string + /** + * C​l​i​e​n​t​ ​c​o​n​f​i​g​u​r​a​t​i​o​n​ ​t​o​k​e​n​ ​a​d​d​e​d + */ + client_configuration_token_added: string } auditModule: { /** @@ -9542,7 +9670,7 @@ export type TranslationFunctions = { header: () => LocalizedString /** *

- If your not using some modules you can disable their visibility. + Hide unused modules.

Read more in documentation. @@ -12399,6 +12527,10 @@ export type TranslationFunctions = { * VPN client failed MFA authentication */ vpn_client_mfa_failed: () => LocalizedString + /** + * Enrollment token added + */ + enrollment_token_added: () => LocalizedString /** * Enrollment started */ @@ -12423,6 +12555,130 @@ export type TranslationFunctions = { * Password reset completed */ password_reset_completed: () => LocalizedString + /** + * VPN location added + */ + vpn_location_added: () => LocalizedString + /** + * VPN location removed + */ + vpn_location_removed: () => LocalizedString + /** + * VPN location modified + */ + vpn_location_modified: () => LocalizedString + /** + * API token added + */ + api_token_added: () => LocalizedString + /** + * API token removed + */ + api_token_removed: () => LocalizedString + /** + * API token renamed + */ + api_token_renamed: () => LocalizedString + /** + * OpenID app added + */ + open_id_app_added: () => LocalizedString + /** + * OpenID app removed + */ + open_id_app_removed: () => LocalizedString + /** + * OpenID app modified + */ + open_id_app_modified: () => LocalizedString + /** + * OpenID app state changed + */ + open_id_app_state_changed: () => LocalizedString + /** + * OpenID provider removed + */ + open_id_provider_removed: () => LocalizedString + /** + * OpenID provider modified + */ + open_id_provider_modified: () => LocalizedString + /** + * Settings updated + */ + settings_updated: () => LocalizedString + /** + * Settings partially updated + */ + settings_updated_partial: () => LocalizedString + /** + * Default branding restored + */ + settings_default_branding_restored: () => LocalizedString + /** + * Groups bulk assigned + */ + groups_bulk_assigned: () => LocalizedString + /** + * Group added + */ + group_added: () => LocalizedString + /** + * Group modified + */ + group_modified: () => LocalizedString + /** + * Group removed + */ + group_removed: () => LocalizedString + /** + * Group member added + */ + group_member_added: () => LocalizedString + /** + * Group member removed + */ + group_member_removed: () => LocalizedString + /** + * Webhook added + */ + web_hook_added: () => LocalizedString + /** + * Webhook modified + */ + web_hook_modified: () => LocalizedString + /** + * Webhook removed + */ + web_hook_removed: () => LocalizedString + /** + * Authentication key added + */ + authentication_key_added: () => LocalizedString + /** + * Authentication key removed + */ + authentication_key_removed: () => LocalizedString + /** + * Authentication key renamed + */ + authentication_key_renamed: () => LocalizedString + /** + * Password changed + */ + password_changed: () => LocalizedString + /** + * Password changed by admin + */ + password_changed_by_admin: () => LocalizedString + /** + * Password reset + */ + password_reset: () => LocalizedString + /** + * Client configuration token added + */ + client_configuration_token_added: () => LocalizedString } auditModule: { /** diff --git a/web/src/pages/activity/types.ts b/web/src/pages/activity/types.ts index 212873765..741f41242 100644 --- a/web/src/pages/activity/types.ts +++ b/web/src/pages/activity/types.ts @@ -38,12 +38,44 @@ export type AuditEventType = | 'vpn_client_connected_mfa' | 'vpn_client_disconnected_mfa' | 'vpn_client_mfa_failed' + | 'enrollment_token_added' | 'enrollment_started' | 'enrollment_device_added' | 'enrollment_completed' | 'password_reset_requested' | 'password_reset_started' - | 'password_reset_completed'; + | 'password_reset_completed' + | 'vpn_location_added' + | 'vpn_location_removed' + | 'vpn_location_modified' + | 'api_token_added' + | 'api_token_removed' + | 'api_token_renamed' + | 'open_id_app_added' + | 'open_id_app_removed' + | 'open_id_app_modified' + | 'open_id_app_state_changed' + | 'open_id_provider_removed' + | 'open_id_provider_modified' + | 'settings_updated' + | 'settings_updated_partial' + | 'settings_default_branding_restored' + | 'groups_bulk_assigned' + | 'group_added' + | 'group_modified' + | 'group_removed' + | 'group_member_added' + | 'group_member_removed' + | 'web_hook_added' + | 'web_hook_modified' + | 'web_hook_removed' + | 'authentication_key_added' + | 'authentication_key_removed' + | 'authentication_key_renamed' + | 'password_changed' + | 'password_changed_by_admin' + | 'password_reset' + | 'client_configuration_token_added'; export const auditEventTypeValues: AuditEventType[] = [ 'user_login', @@ -76,10 +108,42 @@ export const auditEventTypeValues: AuditEventType[] = [ 'vpn_client_connected_mfa', 'vpn_client_disconnected_mfa', 'vpn_client_mfa_failed', + 'enrollment_token_added', 'enrollment_started', 'enrollment_device_added', 'enrollment_completed', 'password_reset_requested', 'password_reset_started', 'password_reset_completed', + 'vpn_location_added', + 'vpn_location_removed', + 'vpn_location_modified', + 'api_token_added', + 'api_token_removed', + 'api_token_renamed', + 'open_id_app_added', + 'open_id_app_removed', + 'open_id_app_modified', + 'open_id_app_state_changed', + 'open_id_provider_removed', + 'open_id_provider_modified', + 'settings_updated', + 'settings_updated_partial', + 'settings_default_branding_restored', + 'groups_bulk_assigned', + 'group_added', + 'group_modified', + 'group_removed', + 'group_member_added', + 'group_member_removed', + 'web_hook_added', + 'web_hook_modified', + 'web_hook_removed', + 'authentication_key_added', + 'authentication_key_removed', + 'authentication_key_renamed', + 'password_changed', + 'password_changed_by_admin', + 'password_reset', + 'client_configuration_token_added', ]; From 4cac1435f4bc24f043cfa6fb24a7620c0115b996 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 25 Jun 2025 16:04:41 +0200 Subject: [PATCH 4/6] Use configured external OIDC Provider for 2FA in client (#1264) * oidc mfa in client * cargo fix * sqlx, fixes * fmt * prevent using oidc mfa if the provider is not configured * revert i18n types * review changes * invalidate user on openid fail * update protos * fmt fix * sqlx * dont log failed precondition errors --- ...e01e900639bc113b7feee2ee52546f8f16b4.json} | 10 +- ...a20e3de28df8100f7c0d476d4182328daeeb.json} | 7 +- .../20250612111316_client_oidc_2fa.down.sql | 1 + .../20250612111316_client_oidc_2fa.up.sql | 1 + .../src/db/models/audit_log/metadata.rs | 3 +- .../src/db/models/audit_log/mod.rs | 3 +- .../defguard_core/src/db/models/settings.rs | 7 +- crates/defguard_core/src/db/models/user.rs | 26 ++-- .../audit_stream/audit_stream_manager.rs | 4 +- .../enterprise/audit_stream/http_stream.rs | 1 - .../enterprise/db/models/openid_provider.rs | 16 +- .../src/enterprise/grpc/desktop_client_mfa.rs | 146 ++++++++++++++++++ .../defguard_core/src/enterprise/grpc/mod.rs | 1 + .../src/enterprise/handlers/audit_stream.rs | 3 +- .../src/enterprise/handlers/openid_login.rs | 79 ++++++++++ .../enterprise/handlers/openid_providers.rs | 54 ++++++- crates/defguard_core/src/events.rs | 24 ++- .../src/grpc/desktop_client_mfa.rs | 123 +++++++++++++-- crates/defguard_core/src/grpc/enrollment.rs | 36 ++++- crates/defguard_core/src/grpc/gateway/mod.rs | 4 +- crates/defguard_core/src/grpc/mod.rs | 46 +++++- crates/defguard_core/src/grpc/utils.rs | 22 ++- .../defguard_core/src/handlers/audit_log.rs | 9 +- .../tests/integration/openid_login.rs | 1 + crates/defguard_event_logger/src/lib.rs | 15 +- crates/defguard_event_logger/src/message.rs | 11 +- crates/defguard_event_router/src/lib.rs | 13 +- proto | 2 +- web/src/i18n/en/index.ts | 5 + web/src/i18n/i18n-types.ts | 20 +++ web/src/i18n/pl/index.ts | 5 + .../components/OpenIdGeneralSettings.tsx | 30 +++- .../components/OpenIdSettingsForm.tsx | 4 + .../OpenIdSettings/components/style.scss | 4 + web/src/pages/settings/style.scss | 3 + 35 files changed, 646 insertions(+), 93 deletions(-) rename .sqlx/{query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json => query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json} (96%) rename .sqlx/{query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json => query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json} (95%) create mode 100644 crates/defguard_core/migrations/20250612111316_client_oidc_2fa.down.sql create mode 100644 crates/defguard_core/migrations/20250612111316_client_oidc_2fa.up.sql create mode 100644 crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs diff --git a/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json b/.sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json similarity index 96% rename from .sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json rename to .sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json index 9f60163ea..53fdfdd55 100644 --- a/.sqlx/query-7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10.json +++ b/.sqlx/query-2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: SyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" FROM \"settings\" WHERE id = 1", + "query": "SELECT openid_enabled, wireguard_enabled, webhooks_enabled, worker_enabled, challenge_template, instance_name, main_logo_url, nav_logo_url, smtp_server, smtp_port, smtp_encryption \"smtp_encryption: _\", smtp_user, smtp_password \"smtp_password?: SecretStringWrapper\", smtp_sender, enrollment_vpn_step_optional, enrollment_welcome_message, enrollment_welcome_email, enrollment_welcome_email_subject, enrollment_use_welcome_message_as_email, uuid, ldap_url, ldap_bind_username, ldap_bind_password \"ldap_bind_password?: SecretStringWrapper\", ldap_group_search_base, ldap_user_search_base, ldap_user_obj_class, ldap_group_obj_class, ldap_username_attr, ldap_groupname_attr, ldap_group_member_attr, ldap_member_attr, openid_create_account, license, gateway_disconnect_notifications_enabled, ldap_use_starttls, ldap_tls_verify_cert, gateway_disconnect_notifications_inactivity_threshold, gateway_disconnect_notifications_reconnect_notification_enabled, ldap_sync_status \"ldap_sync_status: SyncStatus\", ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, ldap_user_rdn_attr, ldap_sync_groups, openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", use_openid_for_mfa FROM \"settings\" WHERE id = 1", "describe": { "columns": [ { @@ -274,6 +274,11 @@ } } } + }, + { + "ordinal": 48, + "name": "use_openid_for_mfa", + "type_info": "Bool" } ], "parameters": { @@ -327,8 +332,9 @@ false, true, false, + false, false ] }, - "hash": "7ddef79c85c3e85b979d5a8a5e50660bcae531c2b8342ae2feffea7454450f10" + "hash": "2eeee174a2a68ff5bd35bab32d35e01e900639bc113b7feee2ee52546f8f16b4" } diff --git a/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json b/.sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json similarity index 95% rename from .sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json rename to .sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json index beabc1823..afb392ccf 100644 --- a/.sqlx/query-3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f.json +++ b/.sqlx/query-f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48 WHERE id = 1", + "query": "UPDATE \"settings\" SET openid_enabled = $1, wireguard_enabled = $2, webhooks_enabled = $3, worker_enabled = $4, challenge_template = $5, instance_name = $6, main_logo_url = $7, nav_logo_url = $8, smtp_server = $9, smtp_port = $10, smtp_encryption = $11, smtp_user = $12, smtp_password = $13, smtp_sender = $14, enrollment_vpn_step_optional = $15, enrollment_welcome_message = $16, enrollment_welcome_email = $17, enrollment_welcome_email_subject = $18, enrollment_use_welcome_message_as_email = $19, uuid = $20, ldap_url = $21, ldap_bind_username = $22, ldap_bind_password = $23, ldap_group_search_base = $24, ldap_user_search_base = $25, ldap_user_obj_class = $26, ldap_group_obj_class = $27, ldap_username_attr = $28, ldap_groupname_attr = $29, ldap_group_member_attr = $30, ldap_member_attr = $31, ldap_use_starttls = $32, ldap_tls_verify_cert = $33, openid_create_account = $34, license = $35, gateway_disconnect_notifications_enabled = $36, gateway_disconnect_notifications_inactivity_threshold = $37, gateway_disconnect_notifications_reconnect_notification_enabled = $38, ldap_sync_status = $39, ldap_enabled = $40, ldap_sync_enabled = $41, ldap_is_authoritative = $42, ldap_sync_interval = $43, ldap_user_auxiliary_obj_classes = $44, ldap_uses_ad = $45, ldap_user_rdn_attr = $46, ldap_sync_groups = $47, openid_username_handling = $48, use_openid_for_mfa = $49 WHERE id = 1", "describe": { "columns": [], "parameters": { @@ -84,10 +84,11 @@ ] } } - } + }, + "Bool" ] }, "nullable": [] }, - "hash": "3491725f35609e9b219c4d613cffd28a14cf37e546dfcabdfd78889dc1ef247f" + "hash": "f3c5a612ced180d9b2014e027d34a20e3de28df8100f7c0d476d4182328daeeb" } diff --git a/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.down.sql b/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.down.sql new file mode 100644 index 000000000..12c99cb1d --- /dev/null +++ b/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.down.sql @@ -0,0 +1 @@ +ALTER TABLE settings DROP COLUMN use_openid_for_mfa; diff --git a/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.up.sql b/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.up.sql new file mode 100644 index 000000000..e937b090f --- /dev/null +++ b/crates/defguard_core/migrations/20250612111316_client_oidc_2fa.up.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN use_openid_for_mfa BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/defguard_core/src/db/models/audit_log/metadata.rs b/crates/defguard_core/src/db/models/audit_log/metadata.rs index 141b7cc48..c8635ed9d 100644 --- a/crates/defguard_core/src/db/models/audit_log/metadata.rs +++ b/crates/defguard_core/src/db/models/audit_log/metadata.rs @@ -13,6 +13,7 @@ use crate::{ audit_stream::{AuditStream, AuditStreamType}, openid_provider::{DirectorySyncTarget, DirectorySyncUserBehavior, OpenIdProvider}, }, + events::ClientMFAMethod, }; #[derive(Serialize)] @@ -159,7 +160,7 @@ pub struct VpnClientMetadata { pub struct VpnClientMfaMetadata { pub location: WireguardNetwork, pub device: Device, - pub method: MFAMethod, + pub method: ClientMFAMethod, } #[derive(Serialize)] diff --git a/crates/defguard_core/src/db/models/audit_log/mod.rs b/crates/defguard_core/src/db/models/audit_log/mod.rs index 0cf327d92..906a9c11a 100644 --- a/crates/defguard_core/src/db/models/audit_log/mod.rs +++ b/crates/defguard_core/src/db/models/audit_log/mod.rs @@ -1,9 +1,10 @@ -use crate::db::{Id, NoId}; use chrono::NaiveDateTime; use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{FromRow, Type}; +use crate::db::{Id, NoId}; + pub mod metadata; #[derive(Clone, Debug, Deserialize, Serialize, Type)] diff --git a/crates/defguard_core/src/db/models/settings.rs b/crates/defguard_core/src/db/models/settings.rs index c2622557d..5747eb76b 100644 --- a/crates/defguard_core/src/db/models/settings.rs +++ b/crates/defguard_core/src/db/models/settings.rs @@ -120,6 +120,7 @@ pub struct Settings { // Whether to create a new account when users try to log in with external OpenID pub openid_create_account: bool, pub openid_username_handling: OpenidUsernameHandling, + pub use_openid_for_mfa: bool, pub license: Option, // Gateway disconnect notifications pub gateway_disconnect_notifications_enabled: bool, @@ -152,7 +153,7 @@ impl Settings { ldap_enabled, ldap_sync_enabled, ldap_is_authoritative, \ ldap_sync_interval, ldap_user_auxiliary_obj_classes, ldap_uses_ad, \ ldap_user_rdn_attr, ldap_sync_groups, \ - openid_username_handling \"openid_username_handling: OpenidUsernameHandling\" \ + openid_username_handling \"openid_username_handling: OpenidUsernameHandling\", use_openid_for_mfa \ FROM \"settings\" WHERE id = 1", ) .fetch_optional(executor) @@ -224,7 +225,8 @@ impl Settings { ldap_uses_ad = $45, \ ldap_user_rdn_attr = $46, \ ldap_sync_groups = $47, \ - openid_username_handling = $48 \ + openid_username_handling = $48, \ + use_openid_for_mfa = $49 \ WHERE id = 1", self.openid_enabled, self.wireguard_enabled, @@ -274,6 +276,7 @@ impl Settings { self.ldap_user_rdn_attr, &self.ldap_sync_groups as &Vec, &self.openid_username_handling as &OpenidUsernameHandling, + self.use_openid_for_mfa, ) .execute(executor) .await?; diff --git a/crates/defguard_core/src/db/models/user.rs b/crates/defguard_core/src/db/models/user.rs index e6db06197..097e2a29b 100644 --- a/crates/defguard_core/src/db/models/user.rs +++ b/crates/defguard_core/src/db/models/user.rs @@ -15,6 +15,7 @@ use rand::{ prelude::Distribution, Rng, }; +use serde::Serialize; use sqlx::{ query, query_as, query_scalar, Error as SqlxError, FromRow, PgConnection, PgExecutor, PgPool, Type, @@ -53,15 +54,7 @@ pub enum MFAMethod { Email, } -impl From for MFAMethod { - fn from(method: MfaMethod) -> Self { - match method { - MfaMethod::Totp => Self::OneTimePassword, - MfaMethod::Email => Self::Email, - } - } -} - +// Web MFA methods impl fmt::Display for MFAMethod { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -77,6 +70,21 @@ impl fmt::Display for MFAMethod { } } +// Client MFA methods +impl fmt::Display for MfaMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + MfaMethod::Totp => "TOTP", + MfaMethod::Email => "Email", + MfaMethod::Oidc => "OIDC", + } + ) + } +} + // User information ready to be sent as part of diagnostic data. #[derive(Serialize)] pub struct UserDiagnostic { diff --git a/crates/defguard_core/src/enterprise/audit_stream/audit_stream_manager.rs b/crates/defguard_core/src/enterprise/audit_stream/audit_stream_manager.rs index 0d7d3cea0..1550a0643 100644 --- a/crates/defguard_core/src/enterprise/audit_stream/audit_stream_manager.rs +++ b/crates/defguard_core/src/enterprise/audit_stream/audit_stream_manager.rs @@ -4,17 +4,15 @@ use bytes::Bytes; use sqlx::PgPool; use tokio::{sync::broadcast::Receiver, task::JoinSet, time::sleep}; use tokio_util::sync::CancellationToken; - use tracing::debug; +use super::AuditStreamReconfigurationNotification; use crate::enterprise::{ audit_stream::http_stream::{run_http_stream_task, HttpAuditStreamConfig}, db::models::audit_stream::{AuditStream, AuditStreamConfig}, is_enterprise_enabled, }; -use super::AuditStreamReconfigurationNotification; - pub async fn run_audit_stream_manager( pool: PgPool, notification: AuditStreamReconfigurationNotification, diff --git a/crates/defguard_core/src/enterprise/audit_stream/http_stream.rs b/crates/defguard_core/src/enterprise/audit_stream/http_stream.rs index 330af98c5..a2dd03a36 100644 --- a/crates/defguard_core/src/enterprise/audit_stream/http_stream.rs +++ b/crates/defguard_core/src/enterprise/audit_stream/http_stream.rs @@ -5,7 +5,6 @@ use bytes::Bytes; use reqwest::tls; use tokio::sync::broadcast::Receiver; use tokio_util::sync::CancellationToken; - use tracing::{debug, error}; use crate::{ diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index f3cb40c7e..44f909ac0 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -1,7 +1,7 @@ use std::fmt; use model_derive::Model; -use sqlx::{query, query_as, Error as SqlxError, PgPool, Type}; +use sqlx::{query, query_as, Error as SqlxError, PgExecutor, PgPool, Type}; use crate::db::{Id, NoId}; @@ -195,7 +195,10 @@ impl OpenIdProvider { } impl OpenIdProvider { - pub async fn find_by_name(pool: &PgPool, name: &str) -> Result, SqlxError> { + pub async fn find_by_name<'e, E>(executor: E, name: &str) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { query_as!( OpenIdProvider, "SELECT id, name, base_url, client_id, client_secret, display_name, \ @@ -207,11 +210,14 @@ impl OpenIdProvider { FROM openidprovider WHERE name = $1", name ) - .fetch_optional(pool) + .fetch_optional(executor) .await } - pub async fn get_current(pool: &PgPool) -> Result, SqlxError> { + pub async fn get_current<'e, E>(executor: E) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { query_as!( OpenIdProvider, "SELECT id, name, base_url, client_id, client_secret, display_name, \ @@ -222,7 +228,7 @@ impl OpenIdProvider { okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match \ FROM openidprovider LIMIT 1" ) - .fetch_optional(pool) + .fetch_optional(executor) .await } } diff --git a/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs new file mode 100644 index 000000000..83d4ecedc --- /dev/null +++ b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs @@ -0,0 +1,146 @@ +use openidconnect::{AuthorizationCode, Nonce}; +use reqwest::Url; +use tonic::Status; + +use crate::{ + enterprise::{ + handlers::openid_login::{extract_state_data, user_from_claims}, + is_enterprise_enabled, + }, + events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, + grpc::{ + desktop_client_mfa::{ClientLoginSession, ClientMfaServer}, + proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod}, + utils::parse_client_info, + }, +}; + +impl ClientMfaServer { + #[instrument(skip_all)] + pub async fn auth_mfa_session_with_oidc( + &mut self, + request: ClientMfaOidcAuthenticateRequest, + info: Option, + ) -> Result<(), Status> { + debug!("Received OIDC MFA authentication request: {request:?}"); + if !is_enterprise_enabled() { + error!("OIDC MFA method requires enterprise feature to be enabled"); + return Err(Status::invalid_argument("OIDC MFA method is not supported")); + } + + let token = extract_state_data(&request.state).ok_or_else(|| { + error!( + "Failed to extract state data from state: {:?}", + request.state + ); + Status::invalid_argument("invalid state data") + })?; + if token.is_empty() { + debug!("Empty token provided in request"); + return Err(Status::invalid_argument("empty token provided")); + } + let pubkey = Self::parse_token(&token)?; + + // fetch login session + let Some(session) = self.sessions.get(&pubkey).cloned() else { + debug!("Client login session not found"); + return Err(Status::invalid_argument("login session not found")); + }; + let ClientLoginSession { + method, + device, + location, + user, + openid_auth_completed, + } = session; + + if openid_auth_completed { + debug!("Client login session already completed"); + return Err(Status::invalid_argument("login session already completed")); + } + + if method != MfaMethod::Oidc { + debug!("Invalid MFA method for OIDC authentication: {method:?}"); + self.sessions.remove(&pubkey); + return Err(Status::invalid_argument("invalid MFA method")); + } + + let (ip, user_agent) = parse_client_info(&info).map_err(Status::internal)?; + let context = BidiRequestContext::new(user.id, user.username.clone(), ip, user_agent); + + let code = AuthorizationCode::new(request.code.clone()); + let url = match Url::parse(&request.callback_url).map_err(|err| { + error!("Invalid redirect URL provided: {err:?}"); + Status::invalid_argument("invalid redirect URL") + }) { + Ok(url) => url, + Err(status) => { + self.sessions.remove(&pubkey); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method, + }, + )), + })?; + return Err(status); + } + }; + + match user_from_claims(&self.pool, Nonce::new(request.nonce.clone()), code, url).await { + Ok(claims_user) => { + // if thats not our user, prevent login + if claims_user.id != user.id { + info!("User {claims_user} tried to use OIDC MFA for another user: {user}"); + self.sessions.remove(&pubkey); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method, + }, + )), + })?; + return Err(Status::unauthenticated("unauthorized")); + } + info!( + "OIDC MFA authentication completed successfully for user: {}", + user.username + ); + } + Err(err) => { + info!("Failed to verify OIDC code: {err:?}"); + self.sessions.remove(&pubkey); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method, + }, + )), + })?; + return Err(Status::unauthenticated("unauthorized")); + } + }; + + self.sessions.insert( + pubkey.clone(), + ClientLoginSession { + method, + device: device.clone(), + location: location.clone(), + user: user.clone(), + openid_auth_completed: true, + }, + ); + + Ok(()) + } +} diff --git a/crates/defguard_core/src/enterprise/grpc/mod.rs b/crates/defguard_core/src/enterprise/grpc/mod.rs index 505916a0a..cc68fd70d 100644 --- a/crates/defguard_core/src/enterprise/grpc/mod.rs +++ b/crates/defguard_core/src/enterprise/grpc/mod.rs @@ -1 +1,2 @@ +pub mod desktop_client_mfa; pub mod polling; diff --git a/crates/defguard_core/src/enterprise/handlers/audit_stream.rs b/crates/defguard_core/src/enterprise/handlers/audit_stream.rs index e920801ee..350925886 100644 --- a/crates/defguard_core/src/enterprise/handlers/audit_stream.rs +++ b/crates/defguard_core/src/enterprise/handlers/audit_stream.rs @@ -5,6 +5,7 @@ use axum::{ use reqwest::StatusCode; use serde_json::json; +use super::LicenseInfo; use crate::{ appstate::AppState, auth::{AdminRole, SessionInfo}, @@ -14,8 +15,6 @@ use crate::{ handlers::{ApiResponse, ApiResult}, }; -use super::LicenseInfo; - pub async fn get_audit_stream( _admin: AdminRole, State(appstate): State, diff --git a/crates/defguard_core/src/enterprise/handlers/openid_login.rs b/crates/defguard_core/src/enterprise/handlers/openid_login.rs index aa63aff4f..f37f7b3aa 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_login.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_login.rs @@ -8,6 +8,7 @@ use axum_extra::{ headers::UserAgent, TypedHeader, }; +use base64::{prelude::BASE64_STANDARD, Engine}; use openidconnect::{ core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata, CoreUserInfoClaims}, AuthorizationCode, ClientId, ClientSecret, CsrfToken, EndpointMaybeSet, EndpointNotSet, @@ -115,6 +116,34 @@ async fn get_provider_metadata(url: &str) -> Result) -> CsrfToken { + let csrf_token = CsrfToken::new_random(); + if let Some(data) = state_data { + let combined = format!("{}.{data}", csrf_token.secret()); + let encoded = BASE64_STANDARD.encode(combined); + CsrfToken::new(encoded) + } else { + csrf_token + } +} + +/// Extract the state data from the provided state. +pub(crate) fn extract_state_data(state: &str) -> Option { + let decoded = BASE64_STANDARD.decode(state).ok()?; + let decoded_str = String::from_utf8(decoded).ok()?; + let result = decoded_str.split_once('.'); + if let Some((part1, part2)) = result { + if part1.is_empty() { + None + } else { + Some(part2.to_string()) + } + } else { + None + } +} + /// Build OpenID Connect client. /// `url`: redirect/callback URL pub(crate) async fn make_oidc_client( @@ -673,4 +702,54 @@ mod test { "averylongnameeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" ); } + + #[test] + fn test_state_build_and_extract() { + // without data + let token = build_state(None); + let decoded = BASE64_STANDARD.decode(token.secret()); + // not base64 encoded + assert!(decoded.is_err()); + assert!(!token.secret().is_empty()); + + // with data + let data = "somedata".to_string(); + let token = build_state(Some(data.clone())); + let decoded = BASE64_STANDARD.decode(token.secret()); + assert!(decoded.is_ok()); + let decoded_str = String::from_utf8(decoded.unwrap()).unwrap(); + let (csrf, state_data) = decoded_str.split_once('.').unwrap(); + assert!(!csrf.is_empty()); + assert_eq!(state_data, data); + + // valid + let data = "my_state_data".to_string(); + let token = build_state(Some(data.clone())); + let extracted = extract_state_data(token.secret()); + assert_eq!(extracted, Some(data)); + + // invalid base64 + let extracted = extract_state_data("not_base64!!"); + assert_eq!(extracted, None); + + // no dot + let encoded = BASE64_STANDARD.encode("no_dot_here"); + let extracted = extract_state_data(&encoded); + assert_eq!(extracted, None); + + // empty first part + let encoded = BASE64_STANDARD.encode(".somedata"); + let extracted = extract_state_data(&encoded); + assert_eq!(extracted, None); + + // empty second part + let encoded = BASE64_STANDARD.encode("csrf."); + let extracted = extract_state_data(&encoded); + assert_eq!(extracted, Some("".to_string())); + + // multiple dots + let encoded = BASE64_STANDARD.encode("csrf.data.with.dots"); + let extracted = extract_state_data(&encoded); + assert_eq!(extracted, Some("data.with.dots".to_string())); + } } diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 92e63f292..4ceb9f124 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -41,6 +41,7 @@ pub struct AddProviderData { pub okta_dirsync_client_id: Option, pub directory_sync_group_match: Option, pub username_handling: OpenidUsernameHandling, + pub use_openid_for_mfa: bool, } #[derive(Debug, Deserialize, Serialize)] @@ -117,6 +118,7 @@ pub async fn add_openid_provider( let mut settings = Settings::get_current_settings(); settings.openid_create_account = provider_data.create_account; + settings.use_openid_for_mfa = provider_data.use_openid_for_mfa; settings.openid_username_handling = provider_data.username_handling; update_current_settings(&appstate.pool, settings).await?; @@ -177,7 +179,6 @@ pub async fn get_current_openid_provider( State(appstate): State, ) -> ApiResult { let settings = Settings::get_current_settings(); - let create_account = settings.openid_create_account; match OpenIdProvider::get_current(&appstate.pool).await? { Some(mut provider) => { // Get rid of it, it should stay on the backend only. @@ -186,7 +187,7 @@ pub async fn get_current_openid_provider( Ok(ApiResponse { json: json!({ "provider": json!(provider), - "settings": json!({ "create_account": create_account, "username_handling": settings.openid_username_handling}), + "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling, "use_openid_for_mfa": settings.use_openid_for_mfa }), }), status: StatusCode::OK, }) @@ -194,7 +195,7 @@ pub async fn get_current_openid_provider( None => Ok(ApiResponse { json: json!({ "provider": null, - "settings": json!({ "create_account": create_account }), + "settings": json!({ "create_account": settings.openid_create_account, "username_handling": settings.openid_username_handling, "use_openid_for_mfa": settings.use_openid_for_mfa }), }), status: StatusCode::NO_CONTENT, }), @@ -213,9 +214,14 @@ pub async fn delete_openid_provider( "User {} deleting OpenID provider {}", session.user.username, provider_data.name ); - let provider = OpenIdProvider::find_by_name(&appstate.pool, &provider_data.name).await?; + let mut trasnaction = appstate.pool.begin().await?; + let provider = OpenIdProvider::find_by_name(&mut *trasnaction, &provider_data.name).await?; if let Some(provider) = provider { - provider.clone().delete(&appstate.pool).await?; + let mut settings = Settings::get_current_settings(); + provider.clone().delete(&mut *trasnaction).await?; + settings.use_openid_for_mfa = false; + update_current_settings(&mut *trasnaction, settings).await?; + trasnaction.commit().await?; info!( "User {} deleted OpenID provider {}", session.user.username, provider.name @@ -240,6 +246,44 @@ pub async fn delete_openid_provider( } } +pub async fn modify_openid_provider( + _license: LicenseInfo, + _admin: AdminRole, + session: SessionInfo, + State(appstate): State, + Json(provider_data): Json, +) -> ApiResult { + debug!( + "User {} modifying OpenID provider {}", + session.user.username, provider_data.name + ); + let mut transaction = appstate.pool.begin().await?; + let provider = OpenIdProvider::find_by_name(&mut *transaction, &provider_data.name).await?; + if let Some(mut provider) = provider { + provider.base_url = provider_data.base_url; + provider.client_id = provider_data.client_id; + provider.client_secret = provider_data.client_secret; + provider.save(&mut *transaction).await?; + info!( + "User {} modified OpenID client {}", + session.user.username, provider.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::OK, + }) + } else { + warn!( + "User {} failed to modify OpenID client {}. Such client does not exist.", + session.user.username, provider_data.name + ); + Ok(ApiResponse { + json: json!({}), + status: StatusCode::NOT_FOUND, + }) + } +} + pub async fn list_openid_providers( _license: LicenseInfo, _admin: AdminRole, diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 8d557f7f0..9550e1857 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -1,5 +1,8 @@ use std::net::IpAddr; +use chrono::{NaiveDateTime, Utc}; +use serde::Serialize; + use crate::{ db::{ models::{authentication_key::AuthenticationKey, oauth2client::OAuth2Client}, @@ -8,8 +11,8 @@ use crate::{ enterprise::db::models::{ api_tokens::ApiToken, audit_stream::AuditStream, openid_provider::OpenIdProvider, }, + grpc::proto::proxy::MfaMethod, }; -use chrono::{NaiveDateTime, Utc}; /// Shared context that needs to be added to every API event /// @@ -327,17 +330,32 @@ pub enum PasswordResetEvent { PasswordResetCompleted, } +pub type ClientMFAMethod = MfaMethod; + +impl Serialize for ClientMFAMethod { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match *self { + MfaMethod::Totp => serializer.serialize_unit_variant("MfaMethod", 0, "Totp"), + MfaMethod::Email => serializer.serialize_unit_variant("MfaMethod", 1, "Email"), + MfaMethod::Oidc => serializer.serialize_unit_variant("MfaMethod", 2, "Oidc"), + } + } +} + #[derive(Debug)] pub enum DesktopClientMfaEvent { Connected { device: Device, location: WireguardNetwork, - method: MFAMethod, + method: ClientMFAMethod, }, Failed { device: Device, location: WireguardNetwork, - method: MFAMethod, + method: ClientMFAMethod, }, } diff --git a/crates/defguard_core/src/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/grpc/desktop_client_mfa.rs index f4d228903..5ca6e876e 100644 --- a/crates/defguard_core/src/grpc/desktop_client_mfa.rs +++ b/crates/defguard_core/src/grpc/desktop_client_mfa.rs @@ -17,8 +17,9 @@ use crate::{ auth::{Claims, ClaimsType}, db::{ models::device::{DeviceInfo, DeviceNetworkInfo, WireguardNetworkDevice}, - Device, GatewayEvent, Id, User, UserInfo, WireguardNetwork, + Device, GatewayEvent, Id, Settings, User, UserInfo, WireguardNetwork, }, + enterprise::{db::models::openid_provider::OpenIdProvider, is_enterprise_enabled}, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, grpc::utils::parse_client_info, handlers::mail::send_email_mfa_code_email, @@ -39,18 +40,20 @@ impl From for Status { } } -struct ClientLoginSession { - method: MfaMethod, - location: WireguardNetwork, - device: Device, - user: User, +#[derive(Clone)] +pub(crate) struct ClientLoginSession { + pub(crate) method: MfaMethod, + pub(crate) location: WireguardNetwork, + pub(crate) device: Device, + pub(crate) user: User, + pub(crate) openid_auth_completed: bool, } -pub(super) struct ClientMfaServer { - pool: PgPool, +pub(crate) struct ClientMfaServer { + pub(crate) pool: PgPool, mail_tx: UnboundedSender, wireguard_tx: Sender, - sessions: HashMap, + pub(crate) sessions: HashMap, bidi_event_tx: UnboundedSender, } @@ -86,7 +89,7 @@ impl ClientMfaServer { } /// Validate JWT and extract client pubkey - fn parse_token(token: &str) -> Result { + pub(crate) fn parse_token(token: &str) -> Result { let claims = Claims::from_jwt(ClaimsType::DesktopClient, token).map_err(|err| { error!("Failed to parse JWT token: {err:?}"); Status::invalid_argument("invalid token") @@ -94,7 +97,7 @@ impl ClientMfaServer { Ok(claims.client_id) } - fn emit_event(&self, event: BidiStreamEvent) -> Result<(), ClientMfaServerError> { + pub(crate) fn emit_event(&self, event: BidiStreamEvent) -> Result<(), ClientMfaServerError> { Ok(self.bidi_event_tx.send(event)?) } @@ -193,7 +196,37 @@ impl ClientMfaServer { Status::internal("unexpected error") })?; } - } + MfaMethod::Oidc => { + if !is_enterprise_enabled() { + error!("OIDC MFA method requires enterprise feature to be enabled"); + return Err(Status::invalid_argument( + "selected MFA method not available", + )); + } + + let settings = Settings::get_current_settings(); + if !settings.use_openid_for_mfa { + error!("OIDC MFA method is not enabled in settings"); + return Err(Status::invalid_argument( + "selected MFA method not available", + )); + } + + if OpenIdProvider::get_current(&self.pool) + .await + .map_err(|err| { + error!("Failed to get current OpenID provider: {err:?}",); + Status::internal("unexpected error") + })? + .is_none() + { + error!("OIDC provider is not configured"); + return Err(Status::invalid_argument( + "selected MFA method not available", + )); + } + } + }; // generate auth token let token = Self::generate_token(&request.pubkey)?; @@ -211,6 +244,7 @@ impl ClientMfaServer { location, device, user, + openid_auth_completed: false, }, ); @@ -237,6 +271,7 @@ impl ClientMfaServer { device, location, user, + openid_auth_completed, } = session; // Prepare event context @@ -246,7 +281,23 @@ impl ClientMfaServer { // validate code match method { MfaMethod::Totp => { - if !user.verify_totp_code(&request.code.to_string()) { + let code = if let Some(code) = request.code { + code.to_string() + } else { + error!("TOTP code not provided in request"); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method: *method, + }, + )), + })?; + return Err(Status::invalid_argument("TOTP code not provided")); + }; + if !user.verify_totp_code(&code) { error!("Provided TOTP code is not valid"); self.emit_event(BidiStreamEvent { context, @@ -254,7 +305,7 @@ impl ClientMfaServer { DesktopClientMfaEvent::Failed { location: location.clone(), device: device.clone(), - method: (*method).into(), + method: *method, }, )), })?; @@ -262,7 +313,23 @@ impl ClientMfaServer { } } MfaMethod::Email => { - if !user.verify_email_mfa_code(&request.code.to_string()) { + let code = if let Some(code) = request.code { + code.to_string() + } else { + error!("Email MFA code not provided in request"); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method: *method, + }, + )), + })?; + return Err(Status::invalid_argument("email MFA code not provided")); + }; + if !user.verify_email_mfa_code(&code) { error!("Provided email code is not valid"); self.emit_event(BidiStreamEvent { context, @@ -270,13 +337,35 @@ impl ClientMfaServer { DesktopClientMfaEvent::Failed { location: location.clone(), device: device.clone(), - method: (*method).into(), + method: *method, }, )), })?; return Err(Status::unauthenticated("unauthorized")); } } + MfaMethod::Oidc => { + if !*openid_auth_completed { + debug!( + "User {user} tried to finish OIDC MFA login but they haven't completed the OIDC authentication yet." + ); + self.emit_event(BidiStreamEvent { + context, + event: BidiStreamEventType::DesktopClientMfa(Box::new( + DesktopClientMfaEvent::Failed { + location: location.clone(), + device: device.clone(), + method: *method, + }, + )), + })?; + return Err(Status::failed_precondition( + "OIDC authentication not completed yet", + )); + } else { + debug!("User {user} is trying to finish OIDC MFA login and the OIDC authentication has already been completed; proceeding."); + } + } } // begin transaction @@ -337,7 +426,7 @@ impl ClientMfaServer { DesktopClientMfaEvent::Connected { location: location.clone(), device: device.clone(), - method: (*method).into(), + method: *method, }, )), })?; diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs index 4b0205e14..f5aaee110 100644 --- a/crates/defguard_core/src/grpc/enrollment.rs +++ b/crates/defguard_core/src/grpc/enrollment.rs @@ -13,7 +13,6 @@ use super::{ }, InstanceInfo, }; -use crate::grpc::utils::parse_client_info; use crate::{ db::{ models::{ @@ -24,11 +23,12 @@ use crate::{ Device, GatewayEvent, Id, Settings, User, }, enterprise::{ - db::models::enterprise_settings::EnterpriseSettings, ldap::utils::ldap_add_user, + db::models::{enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider}, + ldap::utils::ldap_add_user, limits::update_counts, }, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, EnrollmentEvent}, - grpc::utils::{build_device_config_response, new_polling_token}, + grpc::utils::{build_device_config_response, new_polling_token, parse_client_info}, handlers::{mail::send_new_device_added_email, user::check_password_strength}, headers::get_device_info, mail::Mail, @@ -193,7 +193,20 @@ impl EnrollmentServer { "Retrieving instance info for user {}({:?}).", user.username, user.id ); - let instance_info = InstanceInfo::new(settings, &user.username, &enterprise_settings); + + let openid_provider = OpenIdProvider::get_current(&self.pool) + .await + .map_err(|err| { + error!("Failed to get OpenID provider: {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; + + let instance_info = InstanceInfo::new( + settings, + &user.username, + &enterprise_settings, + openid_provider, + ); debug!("Instance info {instance_info:?}"); debug!( @@ -709,11 +722,24 @@ impl EnrollmentServer { info!("Device {} remote configuration done.", device.name); + let openid_provider = OpenIdProvider::get_current(&self.pool) + .await + .map_err(|err| { + error!("Failed to get OpenID provider: {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; + let response = DeviceConfigResponse { device: Some(device.clone().into()), configs: configs.into_iter().map(Into::into).collect(), instance: Some( - InstanceInfo::new(settings, &user.username, &enterprise_settings).into(), + InstanceInfo::new( + settings, + &user.username, + &enterprise_settings, + openid_provider, + ) + .into(), ), token: Some(token.token), }; diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index ee7d5c7cf..290f6517a 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -186,8 +186,8 @@ impl GatewayServer { } pub fn get_client_state_guard( - &self, - ) -> Result, GatewayServerError> { + &'_ self, + ) -> Result, GatewayServerError> { let client_state = self .client_state .lock() diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 63dd1784e..a17968603 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -10,7 +10,7 @@ use std::{ }; use chrono::{NaiveDateTime, Utc}; -use openidconnect::{core::CoreAuthenticationFlow, AuthorizationCode, CsrfToken, Nonce, Scope}; +use openidconnect::{core::CoreAuthenticationFlow, AuthorizationCode, Nonce, Scope}; use reqwest::Url; use serde::Serialize; #[cfg(feature = "worker")] @@ -56,7 +56,7 @@ use crate::{ db::models::{enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider}, directory_sync::sync_user_groups_if_configured, grpc::polling::PollingServer, - handlers::openid_login::{make_oidc_client, user_from_claims}, + handlers::openid_login::{build_state, make_oidc_client, user_from_claims}, is_enterprise_enabled, ldap::utils::ldap_update_user_state, }, @@ -69,7 +69,7 @@ use crate::{ use crate::{auth::ClaimsType, db::GatewayEvent}; mod auth; -mod desktop_client_mfa; +pub(crate) mod desktop_client_mfa; pub mod enrollment; #[cfg(feature = "wireguard")] pub(crate) mod gateway; @@ -646,7 +646,28 @@ pub async fn run_grpc_bidi_stream( Some(core_response::Payload::ClientMfaFinish(response_payload)) } Err(err) => { - error!("client MFA finish error {err}"); + match err.code() { + Code::FailedPrecondition => { + // User not yet done with OIDC authentication. Don't log it as an error. + debug!("Client MFA finish error: {err}"); + } + _ => { + // Log other errors as errors. + error!("Client MFA finish error: {err}"); + } + } + Some(core_response::Payload::CoreError(err.into())) + } + } + } + Some(core_request::Payload::ClientMfaOidcAuthenticate(request)) => { + match client_mfa_server + .auth_mfa_session_with_oidc(request, received.device_info) + .await + { + Ok(()) => Some(core_response::Payload::Empty(())), + Err(err) => { + error!("client MFA OIDC authenticate error {err}"); Some(core_response::Payload::CoreError(err.into())) } } @@ -686,7 +707,7 @@ pub async fn run_grpc_bidi_stream( let (url, csrf_token, nonce) = client .authorize_url( CoreAuthenticationFlow::AuthorizationCode, - CsrfToken::new_random, + || build_state(request.state), Nonce::new_random, ) .add_scope(Scope::new("email".to_string())) @@ -915,6 +936,8 @@ pub struct InstanceInfo { username: String, disable_all_traffic: bool, enterprise_enabled: bool, + use_openid_for_mfa: bool, + openid_display_name: Option, } impl InstanceInfo { @@ -922,8 +945,13 @@ impl InstanceInfo { settings: Settings, username: S, enterprise_settings: &EnterpriseSettings, + openid_provider: Option>, ) -> Self { let config = server_config(); + let openid_display_name = openid_provider + .as_ref() + .map(|provider| provider.display_name.clone()) + .unwrap_or_default(); InstanceInfo { id: settings.uuid, name: settings.instance_name, @@ -932,6 +960,12 @@ impl InstanceInfo { username: username.into(), disable_all_traffic: enterprise_settings.disable_all_traffic, enterprise_enabled: is_enterprise_enabled(), + use_openid_for_mfa: if is_enterprise_enabled() { + settings.use_openid_for_mfa + } else { + false + }, + openid_display_name, } } } @@ -946,6 +980,8 @@ impl From for proto::proxy::InstanceInfo { username: instance.username, disable_all_traffic: instance.disable_all_traffic, enterprise_enabled: instance.enterprise_enabled, + use_openid_for_mfa: instance.use_openid_for_mfa, + openid_display_name: instance.openid_display_name, } } } diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs index 9df56e499..4ced8db38 100644 --- a/crates/defguard_core/src/grpc/utils.rs +++ b/crates/defguard_core/src/grpc/utils.rs @@ -1,5 +1,6 @@ -use sqlx::PgPool; use std::{net::IpAddr, str::FromStr}; + +use sqlx::PgPool; use tonic::Status; use super::{ @@ -15,7 +16,9 @@ use crate::{ }, Device, Id, Settings, User, }, - enterprise::db::models::enterprise_settings::EnterpriseSettings, + enterprise::db::models::{ + enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider, + }, AsCsv, }; @@ -68,6 +71,11 @@ pub(crate) async fn build_device_config_response( ) -> Result { let settings = Settings::get_current_settings(); + let openid_provider = OpenIdProvider::get_current(pool).await.map_err(|err| { + error!("Failed to get OpenID provider: {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; + let networks = WireguardNetwork::all(pool).await.map_err(|err| { error!("Failed to fetch all networks: {err}"); Status::internal(format!("unexpected error: {err}")) @@ -162,7 +170,15 @@ pub(crate) async fn build_device_config_response( Ok(DeviceConfigResponse { device: Some(device.into()), configs, - instance: Some(InstanceInfo::new(settings, &user.username, &enterprise_settings).into()), + instance: Some( + InstanceInfo::new( + settings, + &user.username, + &enterprise_settings, + openid_provider, + ) + .into(), + ), token, }) } diff --git a/crates/defguard_core/src/handlers/audit_log.rs b/crates/defguard_core/src/handlers/audit_log.rs index fada147ee..76cf9ef26 100644 --- a/crates/defguard_core/src/handlers/audit_log.rs +++ b/crates/defguard_core/src/handlers/audit_log.rs @@ -7,17 +7,16 @@ use ipnetwork::IpNetwork; use sqlx::{FromRow, Postgres, QueryBuilder, Type}; use tracing::Instrument; +use super::{ + pagination::{PaginatedApiResponse, PaginatedApiResult, PaginationMeta, PaginationParams}, + DEFAULT_API_PAGE_SIZE, +}; use crate::{ appstate::AppState, auth::SessionInfo, db::{models::audit_log::AuditModule, Id}, }; -use super::{ - pagination::{PaginatedApiResponse, PaginatedApiResult, PaginationMeta, PaginationParams}, - DEFAULT_API_PAGE_SIZE, -}; - #[derive(Debug, Deserialize, Default)] pub struct FilterParams { pub from: Option>, diff --git a/crates/defguard_core/tests/integration/openid_login.rs b/crates/defguard_core/tests/integration/openid_login.rs index 7c4bfab65..68832f5a9 100644 --- a/crates/defguard_core/tests/integration/openid_login.rs +++ b/crates/defguard_core/tests/integration/openid_login.rs @@ -56,6 +56,7 @@ async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) { okta_private_jwk: None, directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, + use_openid_for_mfa: false, }; let response = client diff --git a/crates/defguard_event_logger/src/lib.rs b/crates/defguard_event_logger/src/lib.rs index 500e975c0..30e8085e8 100644 --- a/crates/defguard_event_logger/src/lib.rs +++ b/crates/defguard_event_logger/src/lib.rs @@ -1,12 +1,4 @@ use bytes::Bytes; -use error::EventLoggerError; -use message::{ - DefguardEvent, EnrollmentEvent, EventContext, EventLoggerMessage, LoggerEvent, VpnEvent, -}; -use sqlx::PgPool; -use tokio::sync::mpsc::UnboundedReceiver; -use tracing::{debug, error, info, trace}; - use defguard_core::db::{ models::audit_log::{ metadata::{ @@ -27,6 +19,13 @@ use defguard_core::db::{ }, NoId, }; +use error::EventLoggerError; +use message::{ + DefguardEvent, EnrollmentEvent, EventContext, EventLoggerMessage, LoggerEvent, VpnEvent, +}; +use sqlx::PgPool; +use tokio::sync::mpsc::UnboundedReceiver; +use tracing::{debug, error, info, trace}; pub mod error; pub mod message; diff --git a/crates/defguard_event_logger/src/message.rs b/crates/defguard_event_logger/src/message.rs index 476e5c0ae..1ee5d8087 100644 --- a/crates/defguard_event_logger/src/message.rs +++ b/crates/defguard_event_logger/src/message.rs @@ -1,6 +1,6 @@ -use chrono::NaiveDateTime; use std::net::IpAddr; +use chrono::NaiveDateTime; use defguard_core::{ db::{ models::{authentication_key::AuthenticationKey, oauth2client::OAuth2Client}, @@ -9,7 +9,10 @@ use defguard_core::{ enterprise::db::models::{ api_tokens::ApiToken, audit_stream::AuditStream, openid_provider::OpenIdProvider, }, - events::{ApiRequestContext, BidiRequestContext, GrpcRequestContext, InternalEventContext}, + events::{ + ApiRequestContext, BidiRequestContext, ClientMFAMethod, GrpcRequestContext, + InternalEventContext, + }, }; /// Messages that can be sent to the event logger @@ -271,7 +274,7 @@ pub enum VpnEvent { ConnectedToMfaLocation { location: WireguardNetwork, device: Device, - method: MFAMethod, + method: ClientMFAMethod, }, DisconnectedFromMfaLocation { location: WireguardNetwork, @@ -280,7 +283,7 @@ pub enum VpnEvent { MfaFailed { location: WireguardNetwork, device: Device, - method: MFAMethod, + method: ClientMFAMethod, }, ConnectedToLocation { location: WireguardNetwork, diff --git a/crates/defguard_event_router/src/lib.rs b/crates/defguard_event_router/src/lib.rs index c2c1b73de..a423ef3f9 100644 --- a/crates/defguard_event_router/src/lib.rs +++ b/crates/defguard_event_router/src/lib.rs @@ -28,10 +28,16 @@ //! event_tx.send(event).await.unwrap(); //! ``` -use defguard_core::events::{ApiEvent, BidiStreamEvent, GrpcEvent, InternalEvent}; +use std::sync::Arc; + +use defguard_core::{ + db::GatewayEvent, + events::{ApiEvent, BidiStreamEvent, GrpcEvent, InternalEvent}, + mail::Mail, +}; +use defguard_event_logger::message::{EventContext, EventLoggerMessage, LoggerEvent}; use error::EventRouterError; use events::Event; -use std::sync::Arc; use tokio::sync::{ broadcast::Sender, mpsc::{UnboundedReceiver, UnboundedSender}, @@ -39,9 +45,6 @@ use tokio::sync::{ }; use tracing::{debug, error, info}; -use defguard_core::{db::GatewayEvent, mail::Mail}; -use defguard_event_logger::message::{EventContext, EventLoggerMessage, LoggerEvent}; - mod error; mod events; mod handlers; diff --git a/proto b/proto index 20fe30dfa..eb4ac0620 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 20fe30dfa1c2985bb7a6afe1c74dd9a709e034c6 +Subproject commit eb4ac0620f54bfa58669f2ac61ea5fce5c55b521 diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index 9de72cd3f..fa09a5949 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1238,6 +1238,11 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: 'If this option is enabled, Defguard automatically creates new accounts for users who log in for the first time using an external OpenID provider. Otherwise, the user account must first be created by an administrator.', }, + useOpenIdForMfa: { + label: 'Use external OpenID for client MFA', + helper: + 'When the external OpenID SSO Multi-Factor (MFA) process is enabled, users connecting to VPN locations that require MFA will need to authenticate via their browser using the configured provider for each connection. If this setting is disabled, MFA for those VPN locations will be handled through the internal Defguard SSO system. In that case, users must have TOTP or email-based MFA configured in their profile.', + }, usernameHandling: { label: 'Username handling', helper: diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index fc1b5afde..d28593106 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -3050,6 +3050,16 @@ type RootTranslation = { */ helper: string } + useOpenIdForMfa: { + /** + * U​s​e​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​f​o​r​ ​c​l​i​e​n​t​ ​M​F​A + */ + label: string + /** + * W​h​e​n​ ​t​h​e​ ​e​x​t​e​r​n​a​l​ ​O​p​e​n​I​D​ ​S​S​O​ ​M​u​l​t​i​-​F​a​c​t​o​r​ ​(​M​F​A​)​ ​p​r​o​c​e​s​s​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​c​o​n​n​e​c​t​i​n​g​ ​t​o​ ​V​P​N​ ​l​o​c​a​t​i​o​n​s​ ​t​h​a​t​ ​r​e​q​u​i​r​e​ ​M​F​A​ ​w​i​l​l​ ​n​e​e​d​ ​t​o​ ​a​u​t​h​e​n​t​i​c​a​t​e​ ​v​i​a​ ​t​h​e​i​r​ ​b​r​o​w​s​e​r​ ​u​s​i​n​g​ ​t​h​e​ ​c​o​n​f​i​g​u​r​e​d​ ​p​r​o​v​i​d​e​r​ ​f​o​r​ ​e​a​c​h​ ​c​o​n​n​e​c​t​i​o​n​.​ ​I​f​ ​t​h​i​s​ ​s​e​t​t​i​n​g​ ​i​s​ ​d​i​s​a​b​l​e​d​,​ ​M​F​A​ ​f​o​r​ ​t​h​o​s​e​ ​V​P​N​ ​l​o​c​a​t​i​o​n​s​ ​w​i​l​l​ ​b​e​ ​h​a​n​d​l​e​d​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​i​n​t​e​r​n​a​l​ ​D​e​f​g​u​a​r​d​ ​S​S​O​ ​s​y​s​t​e​m​.​ ​I​n​ ​t​h​a​t​ ​c​a​s​e​,​ ​u​s​e​r​s​ ​m​u​s​t​ ​h​a​v​e​ ​T​O​T​P​ ​o​r​ ​e​m​a​i​l​-​b​a​s​e​d​ ​M​F​A​ ​c​o​n​f​i​g​u​r​e​d​ ​i​n​ ​t​h​e​i​r​ ​p​r​o​f​i​l​e​. + */ + helper: string + } usernameHandling: { /** * U​s​e​r​n​a​m​e​ ​h​a​n​d​l​i​n​g @@ -9391,6 +9401,16 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + useOpenIdForMfa: { + /** + * Use external OpenID for client MFA + */ + label: () => LocalizedString + /** + * When the external OpenID SSO Multi-Factor (MFA) process is enabled, users connecting to VPN locations that require MFA will need to authenticate via their browser using the configured provider for each connection. If this setting is disabled, MFA for those VPN locations will be handled through the internal Defguard SSO system. In that case, users must have TOTP or email-based MFA configured in their profile. + */ + helper: () => LocalizedString + } usernameHandling: { /** * Username handling diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 8b2f55424..384cb9935 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1119,6 +1119,11 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Jeśli ta opcja jest włączona, Defguard automatycznie tworzy nowe konta dla użytkowników, którzy logują się po raz pierwszy za pomocą zewnętrznego dostawcy OpenID. W innym przypadku konto użytkownika musi zostać najpierw utworzone przez administratora.', }, + useOpenIdForMfa: { + label: 'Używaj zewnętrznego OpenID dla MFA klienta', + helper: + 'Gdy zewnętrzny proces Multi-Factor Authentication (MFA) OpenID SSO jest włączony, użytkownicy łączący się z lokalizacjami VPN wymagającymi MFA będą musieli uwierzytelniać się przez swoją przeglądarkę używając skonfigurowanego dostawcy dla każdego połączenia. Jeśli to ustawienie jest wyłączone, MFA dla tych lokalizacji VPN będzie obsługiwane przez wewnętrzny system SSO Defguard. W takim przypadku użytkownicy muszą mieć skonfigurowane TOTP lub MFA oparte na e-mailu.', + }, usernameHandling: { label: 'Obsługa nazw użytkowników', helper: diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx index 815b6bcce..369e2b3b4 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdGeneralSettings.tsx @@ -22,6 +22,14 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) => control, name: 'create_account', }) as boolean; + const use_openid_for_mfa = useWatch({ + control, + name: 'use_openid_for_mfa', + }) as boolean; + const providerName = useWatch({ + control, + name: 'name', + }) as string; const options: SelectOption[] = useMemo( () => [ @@ -44,13 +52,17 @@ export const OpenIdGeneralSettings = ({ isLoading }: { isLoading: boolean }) => [localLL.general.usernameHandling.options], ); + const providerConfigured = useMemo(() => { + return providerName !== ''; + }, [providerName]); + return (

{localLL.general.title()}

{parse(localLL.general.helper())}
-
+
{/* FIXME: Really buggy when using the controller, investigate why */} /> {localLL.general.createAccount.helper()}
+
+ {/* FIXME: Really buggy when using the controller, investigate why */} + { + setValue('use_openid_for_mfa', e); + }} + disabled={isLoading || !providerConfigured} + /> + {localLL.general.useOpenIdForMfa.helper()} +
{ okta_private_jwk: z.string(), okta_dirsync_client_id: z.string(), directory_sync_group_match: z.string(), + use_openid_for_mfa: z.boolean(), }) .superRefine((val, ctx) => { if (val.name === '') { @@ -175,6 +178,7 @@ export const OpenIdSettingsForm = () => { okta_dirsync_client_id: '', directory_sync_group_match: '', username_handling: 'RemoveForbidden', + use_openid_for_mfa: false, }; if (openidData) { diff --git a/web/src/pages/settings/components/OpenIdSettings/components/style.scss b/web/src/pages/settings/components/OpenIdSettings/components/style.scss index ef6f2e6a8..017b6c9d4 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/style.scss +++ b/web/src/pages/settings/components/OpenIdSettings/components/style.scss @@ -7,6 +7,10 @@ padding-bottom: var(--spacing-s); } + .checkbox-padding { + padding-bottom: var(--spacing-s); + } + #sync-not-supported { text-align: center; margin: var(--spacing-s) 0; diff --git a/web/src/pages/settings/style.scss b/web/src/pages/settings/style.scss index 8a7133b6d..81d10988a 100644 --- a/web/src/pages/settings/style.scss +++ b/web/src/pages/settings/style.scss @@ -45,6 +45,9 @@ display: flex; align-items: center; gap: var(--spacing-xs); + .labeled-checkbox { + padding-bottom: 0; + } } section { From 42a2f9ad0bdceadf58cf957240af400129b74afd Mon Sep 17 00:00:00 2001 From: baq04 Date: Mon, 30 Jun 2025 15:17:04 +0200 Subject: [PATCH 5/6] Changed way of sending e-mails --- crates/defguard_core/src/mail.rs | 51 +++++++++++++++++++ .../defguard_core/templates/base_plain.tera | 14 +++++ 2 files changed, 65 insertions(+) create mode 100644 crates/defguard_core/templates/base_plain.tera diff --git a/crates/defguard_core/src/mail.rs b/crates/defguard_core/src/mail.rs index ae58b150a..72a2953ec 100644 --- a/crates/defguard_core/src/mail.rs +++ b/crates/defguard_core/src/mail.rs @@ -237,3 +237,54 @@ pub async fn run_mail_handler(rx: UnboundedReceiver) { info!("Starting mail sending service"); MailHandler::new(rx).run().await; } + +#[cfg(test)] +mod mail_tests { + use super::*; + use crate::templates::{self}; + impl Mail { + fn into_message(self, from: &str) -> Result { + let builder = Message::builder() + .from(Self::mailbox(from)?) + .to(Self::mailbox(&self.to)?) + .subject(self.subject.clone()); + + let mut multipart = + MultiPart::alternative_plain_html(self.content.clone(), self.content); + + if !self.attachments.is_empty() { + for attachment in self.attachments { + multipart = multipart.singlepart(attachment.into()); + } + } + + Ok(builder.multipart(multipart)?) + } + } + + #[test] + fn test_mailer() { + let test_attachment = Attachment { + filename: "test.filename".to_string(), + content: Vec::new(), + content_type: ContentType::TEXT_PLAIN, + }; + + let test = + templates::gateway_disconnected_mail("example_name", "1.2.3.4", "example_name"); + + let test_mail = Mail { + to: "test@example.com".to_string(), + subject: "test".to_string(), + content: test.unwrap(), + attachments: vec![test_attachment], + result_tx: None, + }; + + let message = test_mail.into_message_kuba("sender@example.com").unwrap(); + + let mime_string = message.formatted(); + + println!("content:\n{}", String::from_utf8(mime_string).unwrap()); + } +} diff --git a/crates/defguard_core/templates/base_plain.tera b/crates/defguard_core/templates/base_plain.tera new file mode 100644 index 000000000..db630774a --- /dev/null +++ b/crates/defguard_core/templates/base_plain.tera @@ -0,0 +1,14 @@ +Defguard + +{% block mail_content %} +{% endblock %} + +{% if date_now %} +Date: {{ date_now }} +{% endif %} +{% if ip_address %} +IP Address: {{ ip_address }} +{% endif %} +{% if device_type %} +Device type: {{ device_type }} +{% endif %} From ec84db771d33298e83777f3402791f064cbdcb04 Mon Sep 17 00:00:00 2001 From: baq04 Date: Mon, 30 Jun 2025 16:29:00 +0200 Subject: [PATCH 6/6] Fixed impl --- crates/defguard_core/src/mail.rs | 45 +++++++++----------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/crates/defguard_core/src/mail.rs b/crates/defguard_core/src/mail.rs index 72a2953ec..3a53bd558 100644 --- a/crates/defguard_core/src/mail.rs +++ b/crates/defguard_core/src/mail.rs @@ -103,20 +103,18 @@ impl Mail { .from(Self::mailbox(from)?) .to(Self::mailbox(&self.to)?) .subject(self.subject.clone()); - match self.attachments { - attachments if attachments.is_empty() => Ok(builder - .header(ContentType::TEXT_HTML) - .body(self.content.clone())?), - attachments => { - let mut multipart = MultiPart::mixed().singlepart(SinglePart::html(self.content)); - for attachment in attachments { - multipart = multipart.singlepart(attachment.into()); - } - Ok(builder.multipart(multipart)?) + + let mut multipart = + MultiPart::alternative_plain_html(self.content.clone(), self.content); + + if !self.attachments.is_empty() { + for attachment in self.attachments { + multipart = multipart.singlepart(attachment.into()); } } - } + Ok(builder.multipart(multipart)?) + } /// Builds Mailbox structure from string representing email address fn mailbox(address: &str) -> Result { if let Some((user, domain)) = address.split_once('@') { @@ -242,25 +240,6 @@ pub async fn run_mail_handler(rx: UnboundedReceiver) { mod mail_tests { use super::*; use crate::templates::{self}; - impl Mail { - fn into_message(self, from: &str) -> Result { - let builder = Message::builder() - .from(Self::mailbox(from)?) - .to(Self::mailbox(&self.to)?) - .subject(self.subject.clone()); - - let mut multipart = - MultiPart::alternative_plain_html(self.content.clone(), self.content); - - if !self.attachments.is_empty() { - for attachment in self.attachments { - multipart = multipart.singlepart(attachment.into()); - } - } - - Ok(builder.multipart(multipart)?) - } - } #[test] fn test_mailer() { @@ -281,10 +260,10 @@ mod mail_tests { result_tx: None, }; - let message = test_mail.into_message_kuba("sender@example.com").unwrap(); + let message = test_mail.into_message("sender@example.com").unwrap(); + - let mime_string = message.formatted(); - println!("content:\n{}", String::from_utf8(mime_string).unwrap()); + println!("content:\n{}", String::from_utf8(message.formatted()).unwrap()); } }