From 89ce2f843a40741dbddf71c40c7198ba0196b290 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Tue, 27 Aug 2024 12:34:30 +0200 Subject: [PATCH 01/20] Activate user grpc service returns auth token --- src/db/models/enrollment.rs | 4 ++++ src/grpc/enrollment.rs | 28 +++++++++++++++++++++------- src/grpc/mod.rs | 4 +++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 69d1e2d4ab..39a5e56f0d 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -17,6 +17,10 @@ use crate::{ pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; +pub static AUTH_TOKEN_TYPE: &str = "AUTH"; + +// TODO(jck): move to option / setting? +pub static AUTH_TOKEN_VALIDITY_SECS: u64 = 60 * 60 * 24 * 31; static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; static DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 0307b250aa..0f87f8f12f 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -8,15 +8,17 @@ use tonic::Status; use uaparser::UserAgentParser; use super::proto::{ - ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, - DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, - InitialUserInfo, NewDevice, + ActivateUserRequest, ActivateUserResponse, AdminInfo, Device as ProtoDevice, + DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, + EnrollmentStartResponse, ExistingDevice, InitialUserInfo, NewDevice, }; use crate::{ db::{ models::{ device::{DeviceConfig, DeviceInfo, WireguardNetworkDevice}, - enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE}, + enrollment::{ + Token, TokenError, AUTH_TOKEN_TYPE, AUTH_TOKEN_VALIDITY_SECS, ENROLLMENT_TOKEN_TYPE, + }, wireguard::WireguardNetwork, }, DbPool, Device, GatewayEvent, Settings, User, @@ -219,7 +221,7 @@ impl EnrollmentServer { &self, request: ActivateUserRequest, req_device_info: Option, - ) -> Result<(), Status> { + ) -> Result { debug!("Activating user account: {request:?}"); let enrollment = self.validate_session(request.token.as_deref()).await?; @@ -326,14 +328,26 @@ impl EnrollmentServer { )?; } + // create auth token for further client communication + debug!("Creating auth token for further client communication"); + // TODO(jck): unwrap + let token = Token::new( + user.id.unwrap(), + None, + None, + AUTH_TOKEN_VALIDITY_SECS, + Some(AUTH_TOKEN_TYPE.to_string()), + ); + token.save(&mut transaction).await?; + + debug!("Commiting transaction"); transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); Status::internal("unexpected error") })?; info!("User {} activated", user.username); - - Ok(()) + Ok(super::proto::ActivateUserResponse { token: token.id }) } pub async fn create_device( diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 5f97f606b3..7ca74ac222 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -409,7 +409,9 @@ pub async fn run_grpc_bidi_stream( .activate_user(request, received.device_info) .await { - Ok(()) => Some(core_response::Payload::Empty(())), + Ok(response) => { + Some(core_response::Payload::ActivateUserResponse(response)) + } Err(err) => { error!("activate user error {err}"); Some(core_response::Payload::CoreError(err.into())) From c6582a42a63d29743ee4dbe9c86e22b4eaf49db2 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Aug 2024 10:48:12 +0200 Subject: [PATCH 02/20] Implement basic polling grpc service --- src/config.rs | 4 ++ src/grpc/enrollment.rs | 36 +---------- src/grpc/mod.rs | 55 ++++++++++++++++- src/grpc/polling.rs | 133 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 37 deletions(-) create mode 100644 src/grpc/polling.rs diff --git a/src/config.rs b/src/config.rs index 3b66f28b22..0cc7cb8e6d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -109,6 +109,10 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub enrollment_token_timeout: Duration, + #[arg(long, env = "DEFGUARD_CLIENT_AUTH_TOKEN_TIMEOUT", default_value = "30d")] + #[serde(skip_serializing)] + pub client_auth_token_timeout: Duration, + #[arg(long, env = "DEFGUARD_MFA_CODE_TIMEOUT", default_value = "60s")] #[serde(skip_serializing)] pub mfa_code_timeout: Duration, diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 0f87f8f12f..b8fea83246 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -1,7 +1,7 @@ use std::sync::Arc; +use super::InstanceInfo; use ipnetwork::IpNetwork; -use reqwest::Url; use sqlx::Transaction; use tokio::sync::{broadcast::Sender, mpsc::UnboundedSender}; use tonic::Status; @@ -39,40 +39,6 @@ pub(super) struct EnrollmentServer { ldap_feature_active: bool, } -#[derive(Debug)] -struct InstanceInfo { - id: uuid::Uuid, - name: String, - url: Url, - proxy_url: Url, - username: String, -} - -impl InstanceInfo { - pub fn new>(settings: Settings, username: S) -> Self { - let config = server_config(); - InstanceInfo { - id: settings.uuid, - name: settings.instance_name, - url: config.url.clone(), - proxy_url: config.enrollment_url.clone(), - username: username.into(), - } - } -} - -impl From for super::proto::InstanceInfo { - fn from(instance: InstanceInfo) -> Self { - Self { - name: instance.name, - id: instance.id.to_string(), - url: instance.url.to_string(), - proxy_url: instance.proxy_url.to_string(), - username: instance.username, - } - } -} - impl EnrollmentServer { #[must_use] pub fn new( diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 7ca74ac222..883fc666f8 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -10,6 +10,8 @@ use std::{ }; use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; +use polling::PollingServer; +use reqwest::Url; use serde::Serialize; use thiserror::Error; use tokio::{ @@ -42,7 +44,7 @@ use self::{ worker::{worker_service_server::WorkerServiceServer, WorkerServer}, }; use crate::{ - auth::failed_login::FailedLoginMap, db::AppEvent, + auth::failed_login::FailedLoginMap, db::{AppEvent, Settings}, handlers::mail::send_gateway_disconnected_email, mail::Mail, server_config, }; #[cfg(feature = "worker")] @@ -59,6 +61,7 @@ pub(crate) mod gateway; #[cfg(any(feature = "wireguard", feature = "worker"))] mod interceptor; pub mod password_reset; +pub mod polling; #[cfg(feature = "worker")] pub mod worker; @@ -355,7 +358,8 @@ pub async fn run_grpc_bidi_stream( user_agent_parser, ); let password_reset_server = PasswordResetServer::new(pool.clone(), mail_tx.clone()); - let mut client_mfa_server = ClientMfaServer::new(pool, mail_tx, wireguard_tx); + let mut client_mfa_server = ClientMfaServer::new(pool.clone(), mail_tx, wireguard_tx); + let polling_server = PollingServer::new(pool); let endpoint = Endpoint::from_shared(config.proxy_url.as_deref().unwrap())?; let endpoint = endpoint @@ -507,6 +511,18 @@ pub async fn run_grpc_bidi_stream( } } } + // rpc LocationInfo (LocationInfoRequest) returns (LocationInfoResponse) + Some(core_request::Payload::InstanceInfo(request)) => { + match polling_server.info(request).await { + Ok(response_payload) => { + Some(core_response::Payload::InstanceInfo(response_payload)) + } + Err(err) => { + error!("Instance info error {err}"); + Some(core_response::Payload::CoreError(err.into())) + } + } + } // Reply without payload. None => None, }; @@ -621,3 +637,38 @@ pub struct WorkerDetail { ip: IpAddr, connected: bool, } + +#[derive(Debug)] +pub struct InstanceInfo { + id: uuid::Uuid, + name: String, + url: Url, + proxy_url: Url, + username: String, +} + +impl InstanceInfo { + pub fn new>(settings: Settings, username: S) -> Self { + let config = server_config(); + InstanceInfo { + id: settings.uuid, + name: settings.instance_name, + url: config.url.clone(), + proxy_url: config.enrollment_url.clone(), + username: username.into(), + } + } +} + +impl From for crate::grpc::proto::InstanceInfo { + fn from(instance: InstanceInfo) -> Self { + Self { + name: instance.name, + id: instance.id.to_string(), + url: instance.url.to_string(), + proxy_url: instance.proxy_url.to_string(), + username: instance.username, + } + } +} + diff --git a/src/grpc/polling.rs b/src/grpc/polling.rs new file mode 100644 index 0000000000..4122f6ebe9 --- /dev/null +++ b/src/grpc/polling.rs @@ -0,0 +1,133 @@ +use super::{ + proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse}, + InstanceInfo, +}; +use crate::db::{ + models::{device::WireguardNetworkDevice, enrollment::Token, wireguard::WireguardNetwork}, + DbPool, Device, Settings, +}; +use ipnetwork::IpNetwork; +use tonic::Status; + +use super::proto::{InstanceInfoRequest, InstanceInfoResponse}; + +pub(super) struct PollingServer { + pool: DbPool, +} + +impl PollingServer { + #[must_use] + pub fn new(pool: DbPool) -> Self { + Self { pool } + } + + // check if token provided with request corresponds to a valid session + async fn validate_session(&self, token: Option<&str>) -> Result { + info!("Start validating session. Token {token:?}"); + let Some(token) = token else { + error!("Missing authorization header in request"); + return Err(Status::unauthenticated("Missing authorization header")); + }; + debug!("Validating session token: {token}"); + + let token = Token::find_by_id(&self.pool, token).await?; + debug!("Verify is token valid {token:?}."); + // TODO(jck): proper validation + // if token.is_session_valid(server_config().client_auth_token_timeout.as_secs()) { + // info!("Session validated"); + // Ok(token) + // } else { + // error!("Session expired"); + // Err(Status::unauthenticated("Session expired")) + // } + Ok(token) + } + + /// Get all information needed + /// to update instance information for desktop client + pub async fn info(&self, request: InstanceInfoRequest) -> Result { + // TODO(jck): extract to get_config() function + debug!("Getting network info for device: {:?}", request.pubkey); + let token = self.validate_session(Some(&request.token)).await?; + + // get enrollment user + let user = token.fetch_user(&self.pool).await?; + + Device::validate_pubkey(&request.pubkey).map_err(|_| { + error!("Invalid pubkey {}", request.pubkey); + Status::invalid_argument("invalid pubkey") + })?; + // Find existing device by public key + let device = Device::find_by_pubkey(&self.pool, &request.pubkey) + .await + .map_err(|_| { + error!("Failed to get device by its pubkey: {}", request.pubkey); + Status::internal("unexpected error") + })?; + + let settings = Settings::get_settings(&self.pool).await.map_err(|_| { + error!("Failed to get settings"); + Status::internal("unexpected error") + })?; + + let networks = WireguardNetwork::all(&self.pool).await.map_err(|err| { + error!("Failed to fetch all networks: {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; + + let mut configs: Vec = Vec::new(); + if let Some(device) = device { + for network in networks { + let (Some(device_id), Some(network_id)) = (device.id, network.id) else { + continue; + }; + let wireguard_network_device = + WireguardNetworkDevice::find(&self.pool, device_id, network_id) + .await + .map_err(|err| { + error!("Failed to fetch wireguard network device for device {} and network {}: {err}", device_id, network_id); + Status::internal(format!("unexpected error: {err}")) + })?; + if let Some(wireguard_network_device) = wireguard_network_device { + let allowed_ips = network + .allowed_ips + .iter() + .map(IpNetwork::to_string) + .collect::>() + .join(","); + let config = ProtoDeviceConfig { + config: device.create_config(&network, &wireguard_network_device), + network_id, + network_name: network.name, + assigned_ip: wireguard_network_device.wireguard_ip.to_string(), + endpoint: format!("{}:{}", network.endpoint, network.port), + pubkey: network.pubkey, + allowed_ips, + dns: network.dns, + mfa_enabled: network.mfa_enabled, + keepalive_interval: network.keepalive_interval, + }; + configs.push(config); + } + } + + info!("Device {} configs fetched", device.name); + + let device_config = DeviceConfigResponse { + device: Some(device.into()), + configs, + instance: Some(InstanceInfo::new(settings, &user.username).into()), + }; + + let response = InstanceInfoResponse { + device_config: Some(device_config), + // TODO(jck): actually check enterprise status + is_enterprise: true, + }; + + Ok(response) + } else { + Err(Status::internal("device not found error")) + } + } +} From 2a858e4d02f36924682d127fa9286363071926d9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 30 Aug 2024 10:56:44 +0200 Subject: [PATCH 03/20] Format --- src/config.rs | 6 +++++- src/grpc/mod.rs | 8 +++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0cc7cb8e6d..626b19cee2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -109,7 +109,11 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub enrollment_token_timeout: Duration, - #[arg(long, env = "DEFGUARD_CLIENT_AUTH_TOKEN_TIMEOUT", default_value = "30d")] + #[arg( + long, + env = "DEFGUARD_CLIENT_AUTH_TOKEN_TIMEOUT", + default_value = "30d" + )] #[serde(skip_serializing)] pub client_auth_token_timeout: Duration, diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 883fc666f8..39505af8a6 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -44,8 +44,11 @@ use self::{ worker::{worker_service_server::WorkerServiceServer, WorkerServer}, }; use crate::{ - auth::failed_login::FailedLoginMap, db::{AppEvent, Settings}, - handlers::mail::send_gateway_disconnected_email, mail::Mail, server_config, + auth::failed_login::FailedLoginMap, + db::{AppEvent, Settings}, + handlers::mail::send_gateway_disconnected_email, + mail::Mail, + server_config, }; #[cfg(feature = "worker")] use crate::{ @@ -671,4 +674,3 @@ impl From for crate::grpc::proto::InstanceInfo { } } } - From be93743e5f8311234348056fa87214f5ed87ad28 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 05:26:27 +0200 Subject: [PATCH 04/20] Grpc create_device issues auth token instead of activate_user --- proto | 2 +- src/grpc/enrollment.rs | 32 +++++++++++++++++--------------- src/grpc/mod.rs | 4 +--- src/grpc/polling.rs | 1 + 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/proto b/proto index c71f378472..d9c2c61b1a 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit c71f37847279ee23220fcf9e0e45d2c365b3b8ee +Subproject commit d9c2c61b1aad1b12ab6f1461badba4f072a341e2 diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index b8fea83246..d5694aa968 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -8,7 +8,7 @@ use tonic::Status; use uaparser::UserAgentParser; use super::proto::{ - ActivateUserRequest, ActivateUserResponse, AdminInfo, Device as ProtoDevice, + ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, InitialUserInfo, NewDevice, }; @@ -187,7 +187,7 @@ impl EnrollmentServer { &self, request: ActivateUserRequest, req_device_info: Option, - ) -> Result { + ) -> Result<(), Status> { debug!("Activating user account: {request:?}"); let enrollment = self.validate_session(request.token.as_deref()).await?; @@ -294,18 +294,6 @@ impl EnrollmentServer { )?; } - // create auth token for further client communication - debug!("Creating auth token for further client communication"); - // TODO(jck): unwrap - let token = Token::new( - user.id.unwrap(), - None, - None, - AUTH_TOKEN_VALIDITY_SECS, - Some(AUTH_TOKEN_TYPE.to_string()), - ); - token.save(&mut transaction).await?; - debug!("Commiting transaction"); transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); @@ -313,7 +301,7 @@ impl EnrollmentServer { })?; info!("User {} activated", user.username); - Ok(super::proto::ActivateUserResponse { token: token.id }) + Ok(()) } pub async fn create_device( @@ -415,6 +403,18 @@ impl EnrollmentServer { })?; debug!("Settings {settings:?}"); + // create auth token for further client communication + debug!("Creating auth token for further client communication"); + // TODO(jck): unwrap + let token = Token::new( + user.id.unwrap(), + None, + None, + AUTH_TOKEN_VALIDITY_SECS, + Some(AUTH_TOKEN_TYPE.to_string()), + ); + token.save(&mut transaction).await?; + transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); Status::internal("unexpected error") @@ -449,6 +449,7 @@ impl EnrollmentServer { device: Some(device.into()), configs: configs.into_iter().map(Into::into).collect(), instance: Some(InstanceInfo::new(settings, &user.username).into()), + token: Some(token.id), }; debug!("Created a create device response {response:?}."); @@ -531,6 +532,7 @@ impl EnrollmentServer { device: Some(device.into()), configs, instance: Some(InstanceInfo::new(settings, &user.username).into()), + token: None, }; Ok(response) diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index 39505af8a6..fdb47ea2e4 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -416,9 +416,7 @@ pub async fn run_grpc_bidi_stream( .activate_user(request, received.device_info) .await { - Ok(response) => { - Some(core_response::Payload::ActivateUserResponse(response)) - } + Ok(()) => Some(core_response::Payload::Empty(())), Err(err) => { error!("activate user error {err}"); Some(core_response::Payload::CoreError(err.into())) diff --git a/src/grpc/polling.rs b/src/grpc/polling.rs index 4122f6ebe9..278e41e832 100644 --- a/src/grpc/polling.rs +++ b/src/grpc/polling.rs @@ -117,6 +117,7 @@ impl PollingServer { device: Some(device.into()), configs, instance: Some(InstanceInfo::new(settings, &user.username).into()), + token: None, }; let response = InstanceInfoResponse { From 92a922391a9a42666f6b1b74d64903a1b05e6d22 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 06:09:38 +0200 Subject: [PATCH 05/20] Verify token type during enrollment and password reset --- src/grpc/enrollment.rs | 27 ++++++++++++++++++--------- src/grpc/password_reset.rs | 25 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index d5694aa968..068650e132 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -8,9 +8,9 @@ use tonic::Status; use uaparser::UserAgentParser; use super::proto::{ - ActivateUserRequest, AdminInfo, Device as ProtoDevice, - DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, - EnrollmentStartResponse, ExistingDevice, InitialUserInfo, NewDevice, + ActivateUserRequest, AdminInfo, Device as ProtoDevice, DeviceConfig as ProtoDeviceConfig, + DeviceConfigResponse, EnrollmentStartRequest, EnrollmentStartResponse, ExistingDevice, + InitialUserInfo, NewDevice, }; use crate::{ db::{ @@ -60,20 +60,29 @@ impl EnrollmentServer { // check if token provided with request corresponds to a valid session async fn validate_session(&self, token: Option<&str>) -> Result { - info!("Start validating session. Token {token:?}"); + info!("Validating enrollment session. Token: {token:?}"); let Some(token) = token else { error!("Missing authorization header in request"); return Err(Status::unauthenticated("Missing authorization header")); }; - debug!("Validating session token: {token}"); - let enrollment = Token::find_by_id(&self.pool, token).await?; - debug!("Verify is token valid {enrollment:?}."); + debug!("Found matching token, verifying validity: {enrollment:?}."); + if !enrollment + .token_type + .as_ref() + .is_some_and(|token_type| token_type == ENROLLMENT_TOKEN_TYPE) + { + error!( + "Invalid token type used in enrollment process: {:?}", + enrollment.token_type + ); + return Err(Status::permission_denied("invalid token")); + } if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { - info!("Session validated"); + info!("Enrollment session validated: {enrollment:?}"); Ok(enrollment) } else { - error!("Session expired"); + error!("Enrollment session expired: {enrollment:?}"); Err(Status::unauthenticated("Session expired")) } } diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 98e021bdda..3cf2cecdfa 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -39,24 +39,31 @@ impl PasswordResetServer { // check if token provided with request corresponds to a valid enrollment session async fn validate_session(&self, token: Option<&str>) -> Result { - debug!("Validating enrollment session"); + info!("Validating password reset session. Token: {token:?}"); let Some(token) = token else { error!("Missing authorization header in request"); return Err(Status::unauthenticated("Missing authorization header")); }; - - debug!("Validating enrollment session token: {token}"); let enrollment = Token::find_by_id(&self.pool, token).await?; + debug!("Found matching token, verifying validity: {enrollment:?}."); + if !enrollment + .token_type + .as_ref() + .is_some_and(|token_type| token_type == PASSWORD_RESET_TOKEN_TYPE) + { + error!( + "Invalid token type used in password reset process: {:?}", + enrollment.token_type + ); + return Err(Status::permission_denied("invalid token")); + } if enrollment.is_session_valid(server_config().enrollment_session_timeout.as_secs()) { - info!( - "Enrollment session validated for user {}.", - enrollment.user_id - ); + info!("Password reset session validated: {enrollment:?}.",); Ok(enrollment) } else { - error!("Enrollment session expired"); - Err(Status::unauthenticated("Enrollment session expired")) + error!("Password reset session expired: {enrollment:?}"); + Err(Status::unauthenticated("Session expired")) } } From 7ae77d5b6692c38071412aaeb01210ab84266c42 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 07:23:12 +0200 Subject: [PATCH 06/20] Validate auth token in polling service --- src/config.rs | 8 -- src/db/models/enrollment.rs | 3 - src/grpc/enrollment.rs | 113 +++++----------------------- src/grpc/mod.rs | 1 + src/grpc/password_reset.rs | 4 +- src/grpc/polling.rs | 143 ++++++++---------------------------- src/grpc/utils.rs | 87 ++++++++++++++++++++++ 7 files changed, 139 insertions(+), 220 deletions(-) create mode 100644 src/grpc/utils.rs diff --git a/src/config.rs b/src/config.rs index 626b19cee2..3b66f28b22 100644 --- a/src/config.rs +++ b/src/config.rs @@ -109,14 +109,6 @@ pub struct DefGuardConfig { #[serde(skip_serializing)] pub enrollment_token_timeout: Duration, - #[arg( - long, - env = "DEFGUARD_CLIENT_AUTH_TOKEN_TIMEOUT", - default_value = "30d" - )] - #[serde(skip_serializing)] - pub client_auth_token_timeout: Duration, - #[arg(long, env = "DEFGUARD_MFA_CODE_TIMEOUT", default_value = "60s")] #[serde(skip_serializing)] pub mfa_code_timeout: Duration, diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 39a5e56f0d..5d328b07dd 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -19,9 +19,6 @@ pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; pub static AUTH_TOKEN_TYPE: &str = "AUTH"; -// TODO(jck): move to option / setting? -pub static AUTH_TOKEN_VALIDITY_SECS: u64 = 60 * 60 * 24 * 31; - static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; static DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 068650e132..640245f3ae 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -15,20 +15,11 @@ use super::proto::{ use crate::{ db::{ models::{ - device::{DeviceConfig, DeviceInfo, WireguardNetworkDevice}, - enrollment::{ - Token, TokenError, AUTH_TOKEN_TYPE, AUTH_TOKEN_VALIDITY_SECS, ENROLLMENT_TOKEN_TYPE, - }, - wireguard::WireguardNetwork, + device::{DeviceConfig, DeviceInfo}, + enrollment::{Token, TokenError, AUTH_TOKEN_TYPE, ENROLLMENT_TOKEN_TYPE}, }, DbPool, Device, GatewayEvent, Settings, User, - }, - handlers::{mail::send_new_device_added_email, user::check_password_strength}, - headers::get_device_info, - ldap::utils::ldap_add_user, - mail::Mail, - server_config, - templates::{self, TemplateLocation}, + }, grpc::utils::build_device_config_response, handlers::{mail::send_new_device_added_email, user::check_password_strength}, headers::get_device_info, ldap::utils::ldap_add_user, mail::Mail, server_config, templates::{self, TemplateLocation} }; pub(super) struct EnrollmentServer { @@ -59,7 +50,7 @@ impl EnrollmentServer { } // check if token provided with request corresponds to a valid session - async fn validate_session(&self, token: Option<&str>) -> Result { + async fn validate_session(&self, token: &Option) -> Result { info!("Validating enrollment session. Token: {token:?}"); let Some(token) = token else { error!("Missing authorization header in request"); @@ -198,7 +189,7 @@ impl EnrollmentServer { req_device_info: Option, ) -> Result<(), Status> { debug!("Activating user account: {request:?}"); - let enrollment = self.validate_session(request.token.as_deref()).await?; + let enrollment = self.validate_session(&request.token).await?; let ip_address; let device_info; @@ -319,7 +310,7 @@ impl EnrollmentServer { req_device_info: Option, ) -> Result { debug!("Adding new user device: {request:?}"); - let enrollment = self.validate_session(request.token.as_deref()).await?; + let enrollment = self.validate_session(&request.token).await?; // fetch related users let user = enrollment.fetch_user(&self.pool).await?; @@ -414,12 +405,17 @@ impl EnrollmentServer { // create auth token for further client communication debug!("Creating auth token for further client communication"); - // TODO(jck): unwrap + let user_id = user.id.ok_or_else(|| { + error!("User.id is None, can't create auth token: {user:?}"); + Status::internal("unexpected error") + })?; + let token = Token::new( - user.id.unwrap(), + user_id, None, None, - AUTH_TOKEN_VALIDITY_SECS, + // Auth tokens are valid indefinitely + 0, Some(AUTH_TOKEN_TYPE.to_string()), ); token.save(&mut transaction).await?; @@ -465,89 +461,14 @@ impl EnrollmentServer { Ok(response) } - /// Get all information needed - /// to update instance information for desktop client + /// Get all information needed to update instance information for desktop client pub async fn get_network_info( &self, request: ExistingDevice, ) -> Result { debug!("Getting network info for device: {:?}", request.pubkey); - let enrollment = self.validate_session(request.token.as_deref()).await?; - - // get enrollment user - let user = enrollment.fetch_user(&self.pool).await?; - - Device::validate_pubkey(&request.pubkey).map_err(|_| { - error!("Invalid pubkey {}", request.pubkey); - Status::invalid_argument("invalid pubkey") - })?; - // Find existing device by public key - let device = Device::find_by_pubkey(&self.pool, &request.pubkey) - .await - .map_err(|_| { - error!("Failed to get device by its pubkey: {}", request.pubkey); - Status::internal("unexpected error") - })?; - - let settings = Settings::get_settings(&self.pool).await.map_err(|_| { - error!("Failed to get settings"); - Status::internal("unexpected error") - })?; - - let networks = WireguardNetwork::all(&self.pool).await.map_err(|err| { - error!("Failed to fetch all networks: {err}"); - Status::internal(format!("unexpected error: {err}")) - })?; - - let mut configs: Vec = Vec::new(); - if let Some(device) = device { - for network in networks { - let (Some(device_id), Some(network_id)) = (device.id, network.id) else { - continue; - }; - let wireguard_network_device = - WireguardNetworkDevice::find(&self.pool, device_id, network_id) - .await - .map_err(|err| { - error!("Failed to fetch wireguard network device for device {} and network {}: {err}", device_id, network_id); - Status::internal(format!("unexpected error: {err}")) - })?; - if let Some(wireguard_network_device) = wireguard_network_device { - let allowed_ips = network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); - let config = ProtoDeviceConfig { - config: device.create_config(&network, &wireguard_network_device), - network_id, - network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.to_string(), - endpoint: format!("{}:{}", network.endpoint, network.port), - pubkey: network.pubkey, - allowed_ips, - dns: network.dns, - mfa_enabled: network.mfa_enabled, - keepalive_interval: network.keepalive_interval, - }; - configs.push(config); - } - } - - info!("Device {} configs fetched", device.name); - - let response = DeviceConfigResponse { - device: Some(device.into()), - configs, - instance: Some(InstanceInfo::new(settings, &user.username).into()), - token: None, - }; - - Ok(response) - } else { - Err(Status::internal("device not found error")) - } + let token = self.validate_session(&request.token).await?; + build_device_config_response(&self.pool, &token, &request.pubkey).await } } diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index fdb47ea2e4..bd86236c74 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -65,6 +65,7 @@ pub(crate) mod gateway; mod interceptor; pub mod password_reset; pub mod polling; +pub(crate) mod utils; #[cfg(feature = "worker")] pub mod worker; diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 3cf2cecdfa..2e90a2b11a 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -38,7 +38,7 @@ impl PasswordResetServer { } // check if token provided with request corresponds to a valid enrollment session - async fn validate_session(&self, token: Option<&str>) -> Result { + async fn validate_session(&self, token: &Option) -> Result { info!("Validating password reset session. Token: {token:?}"); let Some(token) = token else { error!("Missing authorization header in request"); @@ -214,7 +214,7 @@ impl PasswordResetServer { req_device_info: Option, ) -> Result<(), Status> { debug!("Starting password reset: {request:?}"); - let enrollment = self.validate_session(request.token.as_deref()).await?; + let enrollment = self.validate_session(&request.token).await?; let ip_address; let user_agent; diff --git a/src/grpc/polling.rs b/src/grpc/polling.rs index 278e41e832..978506f7f0 100644 --- a/src/grpc/polling.rs +++ b/src/grpc/polling.rs @@ -1,12 +1,10 @@ -use super::{ - proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse}, - InstanceInfo, +use crate::{ + db::{ + models::enrollment::{Token, AUTH_TOKEN_TYPE}, + DbPool, + }, + grpc::utils::build_device_config_response, }; -use crate::db::{ - models::{device::WireguardNetworkDevice, enrollment::Token, wireguard::WireguardNetwork}, - DbPool, Device, Settings, -}; -use ipnetwork::IpNetwork; use tonic::Status; use super::proto::{InstanceInfoRequest, InstanceInfoResponse}; @@ -22,113 +20,36 @@ impl PollingServer { } // check if token provided with request corresponds to a valid session - async fn validate_session(&self, token: Option<&str>) -> Result { - info!("Start validating session. Token {token:?}"); - let Some(token) = token else { - error!("Missing authorization header in request"); - return Err(Status::unauthenticated("Missing authorization header")); - }; - debug!("Validating session token: {token}"); - + async fn validate_session(&self, token: &str) -> Result { + debug!("Validating auth session. Token: {token}"); let token = Token::find_by_id(&self.pool, token).await?; - debug!("Verify is token valid {token:?}."); - // TODO(jck): proper validation - // if token.is_session_valid(server_config().client_auth_token_timeout.as_secs()) { - // info!("Session validated"); - // Ok(token) - // } else { - // error!("Session expired"); - // Err(Status::unauthenticated("Session expired")) - // } - Ok(token) + debug!("Found matching token, verifying validity: {token:?}."); + // Auth tokens are valid indefinitely + if token + .token_type + .as_ref() + .is_some_and(|token_type| token_type == AUTH_TOKEN_TYPE) + { + Ok(token) + } else { + error!( + "Invalid token type used in polling process: {:?}", + token.token_type + ); + Err(Status::permission_denied("invalid token")) + } } - /// Get all information needed - /// to update instance information for desktop client + /// Get all information needed to update instance information for desktop client pub async fn info(&self, request: InstanceInfoRequest) -> Result { - // TODO(jck): extract to get_config() function debug!("Getting network info for device: {:?}", request.pubkey); - let token = self.validate_session(Some(&request.token)).await?; - - // get enrollment user - let user = token.fetch_user(&self.pool).await?; - - Device::validate_pubkey(&request.pubkey).map_err(|_| { - error!("Invalid pubkey {}", request.pubkey); - Status::invalid_argument("invalid pubkey") - })?; - // Find existing device by public key - let device = Device::find_by_pubkey(&self.pool, &request.pubkey) - .await - .map_err(|_| { - error!("Failed to get device by its pubkey: {}", request.pubkey); - Status::internal("unexpected error") - })?; - - let settings = Settings::get_settings(&self.pool).await.map_err(|_| { - error!("Failed to get settings"); - Status::internal("unexpected error") - })?; - - let networks = WireguardNetwork::all(&self.pool).await.map_err(|err| { - error!("Failed to fetch all networks: {err}"); - Status::internal(format!("unexpected error: {err}")) - })?; - - let mut configs: Vec = Vec::new(); - if let Some(device) = device { - for network in networks { - let (Some(device_id), Some(network_id)) = (device.id, network.id) else { - continue; - }; - let wireguard_network_device = - WireguardNetworkDevice::find(&self.pool, device_id, network_id) - .await - .map_err(|err| { - error!("Failed to fetch wireguard network device for device {} and network {}: {err}", device_id, network_id); - Status::internal(format!("unexpected error: {err}")) - })?; - if let Some(wireguard_network_device) = wireguard_network_device { - let allowed_ips = network - .allowed_ips - .iter() - .map(IpNetwork::to_string) - .collect::>() - .join(","); - let config = ProtoDeviceConfig { - config: device.create_config(&network, &wireguard_network_device), - network_id, - network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ip.to_string(), - endpoint: format!("{}:{}", network.endpoint, network.port), - pubkey: network.pubkey, - allowed_ips, - dns: network.dns, - mfa_enabled: network.mfa_enabled, - keepalive_interval: network.keepalive_interval, - }; - configs.push(config); - } - } - - info!("Device {} configs fetched", device.name); - - let device_config = DeviceConfigResponse { - device: Some(device.into()), - configs, - instance: Some(InstanceInfo::new(settings, &user.username).into()), - token: None, - }; - - let response = InstanceInfoResponse { - device_config: Some(device_config), - // TODO(jck): actually check enterprise status - is_enterprise: true, - }; - - Ok(response) - } else { - Err(Status::internal("device not found error")) - } + let token = self.validate_session(&request.token).await?; + let device_config = + build_device_config_response(&self.pool, &token, &request.pubkey).await?; + Ok(InstanceInfoResponse { + device_config: Some(device_config), + // TODO(jck): actually check enterprise status + is_enterprise: true, + }) } } diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs new file mode 100644 index 0000000000..2665d6fabc --- /dev/null +++ b/src/grpc/utils.rs @@ -0,0 +1,87 @@ +use super::InstanceInfo; +use ipnetwork::IpNetwork; +use tonic::Status; + +use super::proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse}; +use crate::db::{ + models::{device::WireguardNetworkDevice, enrollment::Token, wireguard::WireguardNetwork}, + DbPool, Device, Settings, +}; + +pub(crate) async fn build_device_config_response( + pool: &DbPool, + token: &Token, + pubkey: &str, +) -> Result { + // get enrollment user + let user = token.fetch_user(pool).await?; + + Device::validate_pubkey(pubkey).map_err(|_| { + error!("Invalid pubkey {pubkey}"); + Status::invalid_argument("invalid pubkey") + })?; + // Find existing device by public key + let device = Device::find_by_pubkey(pool, pubkey).await.map_err(|_| { + error!("Failed to get device by its pubkey: {pubkey}"); + Status::internal("unexpected error") + })?; + + let settings = Settings::get_settings(pool).await.map_err(|_| { + error!("Failed to get settings"); + Status::internal("unexpected error") + })?; + + let networks = WireguardNetwork::all(pool).await.map_err(|err| { + error!("Failed to fetch all networks: {err}"); + Status::internal(format!("unexpected error: {err}")) + })?; + + let mut configs: Vec = Vec::new(); + let Some(device) = device else { + return Err(Status::internal("device not found error")); + }; + for network in networks { + let (Some(device_id), Some(network_id)) = (device.id, network.id) else { + continue; + }; + let wireguard_network_device = WireguardNetworkDevice::find(pool, device_id, network_id) + .await + .map_err(|err| { + error!( + "Failed to fetch wireguard network device for device {} and network {}: {err}", + device_id, network_id + ); + Status::internal(format!("unexpected error: {err}")) + })?; + if let Some(wireguard_network_device) = wireguard_network_device { + let allowed_ips = network + .allowed_ips + .iter() + .map(IpNetwork::to_string) + .collect::>() + .join(","); + let config = ProtoDeviceConfig { + config: device.create_config(&network, &wireguard_network_device), + network_id, + network_name: network.name, + assigned_ip: wireguard_network_device.wireguard_ip.to_string(), + endpoint: format!("{}:{}", network.endpoint, network.port), + pubkey: network.pubkey, + allowed_ips, + dns: network.dns, + mfa_enabled: network.mfa_enabled, + keepalive_interval: network.keepalive_interval, + }; + configs.push(config); + } + } + + info!("Device {} configs fetched", device.name); + + Ok(DeviceConfigResponse { + device: Some(device.into()), + configs, + instance: Some(InstanceInfo::new(settings, &user.username).into()), + token: None, + }) +} From c70aa77fc2637021ca3e34f816f85248697e7d24 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 07:46:52 +0200 Subject: [PATCH 07/20] Move polling grpc server to enterprise module --- src/enterprise/grpc/mod.rs | 1 + src/{ => enterprise}/grpc/polling.rs | 4 ++-- src/enterprise/mod.rs | 1 + src/grpc/mod.rs | 3 +-- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 src/enterprise/grpc/mod.rs rename src/{ => enterprise}/grpc/polling.rs (94%) diff --git a/src/enterprise/grpc/mod.rs b/src/enterprise/grpc/mod.rs new file mode 100644 index 0000000000..505916a0a5 --- /dev/null +++ b/src/enterprise/grpc/mod.rs @@ -0,0 +1 @@ +pub mod polling; diff --git a/src/grpc/polling.rs b/src/enterprise/grpc/polling.rs similarity index 94% rename from src/grpc/polling.rs rename to src/enterprise/grpc/polling.rs index 978506f7f0..c7df7f0b5d 100644 --- a/src/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -7,9 +7,9 @@ use crate::{ }; use tonic::Status; -use super::proto::{InstanceInfoRequest, InstanceInfoResponse}; +use crate::grpc::proto::{InstanceInfoRequest, InstanceInfoResponse}; -pub(super) struct PollingServer { +pub struct PollingServer { pool: DbPool, } diff --git a/src/enterprise/mod.rs b/src/enterprise/mod.rs index 6af234a386..dadca5b4ce 100644 --- a/src/enterprise/mod.rs +++ b/src/enterprise/mod.rs @@ -1,3 +1,4 @@ pub mod db; +pub mod grpc; pub mod handlers; pub mod license; diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index bd86236c74..f053933c91 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -10,7 +10,7 @@ use std::{ }; use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; -use polling::PollingServer; +use crate::enterprise::grpc::polling::PollingServer; use reqwest::Url; use serde::Serialize; use thiserror::Error; @@ -64,7 +64,6 @@ pub(crate) mod gateway; #[cfg(any(feature = "wireguard", feature = "worker"))] mod interceptor; pub mod password_reset; -pub mod polling; pub(crate) mod utils; #[cfg(feature = "worker")] pub mod worker; From 79cd4ff5e7621c4840184a106e1d9d806987cb9a Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 08:10:48 +0200 Subject: [PATCH 08/20] Make polling service enterprise only --- src/enterprise/grpc/polling.rs | 10 ++++++---- src/enterprise/license.rs | 2 +- src/grpc/enrollment.rs | 2 +- src/grpc/password_reset.rs | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index c7df7f0b5d..a1859752c6 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -3,6 +3,7 @@ use crate::{ models::enrollment::{Token, AUTH_TOKEN_TYPE}, DbPool, }, + enterprise::license::{get_cached_license, validate_license}, grpc::utils::build_device_config_response, }; use tonic::Status; @@ -19,9 +20,12 @@ impl PollingServer { Self { pool } } - // check if token provided with request corresponds to a valid session + /// Checks if token provided with request corresponds to a valid auth session async fn validate_session(&self, token: &str) -> Result { debug!("Validating auth session. Token: {token}"); + // Polling service is enterprise-only, check the lincense + validate_license(get_cached_license().as_ref()) + .map_err(|_| Status::permission_denied("no valid license"))?; let token = Token::find_by_id(&self.pool, token).await?; debug!("Found matching token, verifying validity: {token:?}."); // Auth tokens are valid indefinitely @@ -40,7 +44,7 @@ impl PollingServer { } } - /// Get all information needed to update instance information for desktop client + /// Prepares instance info for polling requests. Enterprise only. pub async fn info(&self, request: InstanceInfoRequest) -> Result { debug!("Getting network info for device: {:?}", request.pubkey); let token = self.validate_session(&request.token).await?; @@ -48,8 +52,6 @@ impl PollingServer { build_device_config_response(&self.pool, &token, &request.pubkey).await?; Ok(InstanceInfoResponse { device_config: Some(device_config), - // TODO(jck): actually check enterprise status - is_enterprise: true, }) } } diff --git a/src/enterprise/license.rs b/src/enterprise/license.rs index acb68ef7dc..498c4f3733 100644 --- a/src/enterprise/license.rs +++ b/src/enterprise/license.rs @@ -391,7 +391,7 @@ async fn renew_license(db_pool: &DbPool) -> Result { /// /// This function checks the following two things: /// 1. Does the cached license exist -/// 2. Does the cached license is past its maximum expiry date +/// 2. Is the cached license past its maximum expiry date pub fn validate_license(license: Option<&License>) -> Result<(), LicenseError> { debug!("Validating if the license is present and not expired..."); match license { diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 640245f3ae..9aa2d5f085 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -49,7 +49,7 @@ impl EnrollmentServer { } } - // check if token provided with request corresponds to a valid session + /// Checks if token provided with request corresponds to a valid enrollment session async fn validate_session(&self, token: &Option) -> Result { info!("Validating enrollment session. Token: {token:?}"); let Some(token) = token else { diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 2e90a2b11a..bea8fc54be 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -37,7 +37,7 @@ impl PasswordResetServer { } } - // check if token provided with request corresponds to a valid enrollment session + /// Checks if token provided with request corresponds to a valid password reset session async fn validate_session(&self, token: &Option) -> Result { info!("Validating password reset session. Token: {token:?}"); let Some(token) = token else { From 61e71b8ec973957853fec0d679366d4098b77000 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 08:39:32 +0200 Subject: [PATCH 09/20] Polling service logs when no valid license --- src/enterprise/grpc/polling.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index a1859752c6..e5e8905b7e 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -23,9 +23,13 @@ impl PollingServer { /// Checks if token provided with request corresponds to a valid auth session async fn validate_session(&self, token: &str) -> Result { debug!("Validating auth session. Token: {token}"); + // Polling service is enterprise-only, check the lincense - validate_license(get_cached_license().as_ref()) - .map_err(|_| Status::permission_denied("no valid license"))?; + if validate_license(get_cached_license().as_ref()).is_err() { + debug!("No valid license, denying instance polling info"); + return Err(Status::permission_denied("no valid license")); + } + let token = Token::find_by_id(&self.pool, token).await?; debug!("Found matching token, verifying validity: {token:?}."); // Auth tokens are valid indefinitely From b500156a923b7f9673dcd674db6606d830fa737d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 08:55:09 +0200 Subject: [PATCH 10/20] Update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index d9c2c61b1a..af18eaffb5 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit d9c2c61b1aad1b12ab6f1461badba4f072a341e2 +Subproject commit af18eaffb5a5800ac068040fd1fcd0414843f204 From f64912b8a03df2d2a70b1e5f6f9dac1c61863661 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 17:23:47 +0200 Subject: [PATCH 11/20] Rename AUTH_TOKEN_TYPE -> POLLING_TOKEN_TYPE --- src/db/models/enrollment.rs | 2 +- src/enterprise/grpc/polling.rs | 4 ++-- src/grpc/enrollment.rs | 14 ++++++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 5d328b07dd..4d03060cd7 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -17,7 +17,7 @@ use crate::{ pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; -pub static AUTH_TOKEN_TYPE: &str = "AUTH"; +pub static POLLING_TOKEN_TYPE: &str = "POLLING"; static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; static DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index e5e8905b7e..9accb10acc 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -1,6 +1,6 @@ use crate::{ db::{ - models::enrollment::{Token, AUTH_TOKEN_TYPE}, + models::enrollment::{Token, POLLING_TOKEN_TYPE}, DbPool, }, enterprise::license::{get_cached_license, validate_license}, @@ -36,7 +36,7 @@ impl PollingServer { if token .token_type .as_ref() - .is_some_and(|token_type| token_type == AUTH_TOKEN_TYPE) + .is_some_and(|token_type| token_type == POLLING_TOKEN_TYPE) { Ok(token) } else { diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 9aa2d5f085..25d5476948 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -16,10 +16,17 @@ use crate::{ db::{ models::{ device::{DeviceConfig, DeviceInfo}, - enrollment::{Token, TokenError, AUTH_TOKEN_TYPE, ENROLLMENT_TOKEN_TYPE}, + enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE, POLLING_TOKEN_TYPE}, }, DbPool, Device, GatewayEvent, Settings, User, - }, grpc::utils::build_device_config_response, handlers::{mail::send_new_device_added_email, user::check_password_strength}, headers::get_device_info, ldap::utils::ldap_add_user, mail::Mail, server_config, templates::{self, TemplateLocation} + }, + grpc::utils::build_device_config_response, + handlers::{mail::send_new_device_added_email, user::check_password_strength}, + headers::get_device_info, + ldap::utils::ldap_add_user, + mail::Mail, + server_config, + templates::{self, TemplateLocation}, }; pub(super) struct EnrollmentServer { @@ -294,7 +301,6 @@ impl EnrollmentServer { )?; } - debug!("Commiting transaction"); transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); Status::internal("unexpected error") @@ -416,7 +422,7 @@ impl EnrollmentServer { None, // Auth tokens are valid indefinitely 0, - Some(AUTH_TOKEN_TYPE.to_string()), + Some(POLLING_TOKEN_TYPE.to_string()), ); token.save(&mut transaction).await?; From 1905e8ec39a1ab44ebd618214be21c5e2646d33e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 5 Sep 2024 17:39:23 +0200 Subject: [PATCH 12/20] Update protos && cargo fmt --- proto | 2 +- src/grpc/mod.rs | 2 +- src/grpc/utils.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/proto b/proto index af18eaffb5..6c89f195e6 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit af18eaffb5a5800ac068040fd1fcd0414843f204 +Subproject commit 6c89f195e6d9de08a13c97ccf09550873a845d4f diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs index f053933c91..a7be80e6c5 100644 --- a/src/grpc/mod.rs +++ b/src/grpc/mod.rs @@ -9,8 +9,8 @@ use std::{ sync::{Arc, Mutex}, }; -use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use crate::enterprise::grpc::polling::PollingServer; +use chrono::{Duration as ChronoDuration, NaiveDateTime, Utc}; use reqwest::Url; use serde::Serialize; use thiserror::Error; diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index 2665d6fabc..0543d9f491 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -42,8 +42,8 @@ pub(crate) async fn build_device_config_response( }; for network in networks { let (Some(device_id), Some(network_id)) = (device.id, network.id) else { - continue; - }; + continue; + }; let wireguard_network_device = WireguardNetworkDevice::find(pool, device_id, network_id) .await .map_err(|err| { From 8637abce904325ccfd8b630d83d7a6fb81b5d350 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 11:42:24 +0200 Subject: [PATCH 13/20] PollingToken struct, create during enrollment `create_device` --- .../20240906090729_polling_token.down.sql | 1 + .../20240906090729_polling_token.up.sql | 7 ++++ src/db/models/mod.rs | 1 + src/db/models/polling_token.rs | 38 +++++++++++++++++++ src/grpc/enrollment.rs | 30 +++++++-------- 5 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 migrations/20240906090729_polling_token.down.sql create mode 100644 migrations/20240906090729_polling_token.up.sql create mode 100644 src/db/models/polling_token.rs diff --git a/migrations/20240906090729_polling_token.down.sql b/migrations/20240906090729_polling_token.down.sql new file mode 100644 index 0000000000..bb7a045ee9 --- /dev/null +++ b/migrations/20240906090729_polling_token.down.sql @@ -0,0 +1 @@ +DROP TABLE pollingtoken; diff --git a/migrations/20240906090729_polling_token.up.sql b/migrations/20240906090729_polling_token.up.sql new file mode 100644 index 0000000000..933a426f90 --- /dev/null +++ b/migrations/20240906090729_polling_token.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE pollingtoken ( + id bigserial PRIMARY KEY, + token TEXT NOT NULL, + device_id bigint NOT NULL, + created_at timestamp without time zone NOT NULL DEFAULT now(), + FOREIGN KEY(device_id) REFERENCES "device"(id) ON DELETE CASCADE +); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 73722c1026..0a8f4d7837 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -12,6 +12,7 @@ pub mod oauth2authorizedapp; pub mod oauth2client; #[cfg(feature = "openid")] pub mod oauth2token; +pub mod polling_token; pub mod session; pub mod settings; pub mod user; diff --git a/src/db/models/polling_token.rs b/src/db/models/polling_token.rs new file mode 100644 index 0000000000..3227a26854 --- /dev/null +++ b/src/db/models/polling_token.rs @@ -0,0 +1,38 @@ +use chrono::NaiveDateTime; +use model_derive::Model; +use sqlx::{query_as, Error as SqlxError}; + +use crate::random::gen_alphanumeric; + +use super::DbPool; + +// Token used for polling requests. +#[derive(Clone, Debug, Model)] +pub struct PollingToken { + pub id: Option, + pub token: String, + pub device_id: i64, + pub created_at: Option, +} + +impl PollingToken { + pub fn new(device_id: i64) -> Self { + Self { + id: None, + device_id, + token: gen_alphanumeric(32), + created_at: None, + } + } + + pub async fn find(pool: &DbPool, token: &str) -> Result, SqlxError> { + query_as!( + Self, + "SELECT id, token, device_id, created_at + FROM pollingtoken WHERE token = $1", + token + ) + .fetch_optional(pool) + .await + } +} diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 25d5476948..058117b89c 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -16,7 +16,7 @@ use crate::{ db::{ models::{ device::{DeviceConfig, DeviceInfo}, - enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE, POLLING_TOKEN_TYPE}, + enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE}, polling_token::PollingToken, }, DbPool, Device, GatewayEvent, Settings, User, }, @@ -409,22 +409,18 @@ impl EnrollmentServer { })?; debug!("Settings {settings:?}"); - // create auth token for further client communication - debug!("Creating auth token for further client communication"); - let user_id = user.id.ok_or_else(|| { - error!("User.id is None, can't create auth token: {user:?}"); - Status::internal("unexpected error") - })?; - - let token = Token::new( - user_id, - None, - None, - // Auth tokens are valid indefinitely - 0, - Some(POLLING_TOKEN_TYPE.to_string()), + // create polling token for further client communication + debug!("Creating polling token for further client communication"); + let mut token = PollingToken::new( + device.id.ok_or_else(|| { + error!("No device id"); + Status::internal("unexpected error") + })?, ); - token.save(&mut transaction).await?; + token.save(&mut *transaction).await.map_err(|err| { + error!("Failed to save PollingToken: {err}"); + Status::internal("failed to save polling token") + })?; transaction.commit().await.map_err(|_| { error!("Failed to commit transaction"); @@ -460,7 +456,7 @@ impl EnrollmentServer { device: Some(device.into()), configs: configs.into_iter().map(Into::into).collect(), instance: Some(InstanceInfo::new(settings, &user.username).into()), - token: Some(token.id), + token: Some(token.token), }; debug!("Created a create device response {response:?}."); From 79f4c2c6caf961e0a6804fb80e256bb178d1cd44 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 13:37:32 +0200 Subject: [PATCH 14/20] Use PollingToken in enrollment and polling service --- src/db/models/enrollment.rs | 1 - src/db/models/user.rs | 18 +++++++++++++ src/enterprise/grpc/polling.rs | 47 +++++++++++++++++----------------- src/grpc/enrollment.rs | 4 +-- src/grpc/utils.rs | 16 +++++++----- 5 files changed, 52 insertions(+), 34 deletions(-) diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 4d03060cd7..69d1e2d4ab 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -17,7 +17,6 @@ use crate::{ pub static ENROLLMENT_TOKEN_TYPE: &str = "ENROLLMENT"; pub static PASSWORD_RESET_TOKEN_TYPE: &str = "PASSWORD_RESET"; -pub static POLLING_TOKEN_TYPE: &str = "POLLING"; static ENROLLMENT_START_MAIL_SUBJECT: &str = "Defguard user enrollment"; static DESKTOP_START_MAIL_SUBJECT: &str = "Defguard desktop client configuration"; diff --git a/src/db/models/user.rs b/src/db/models/user.rs index b9b5948652..18928b7bc4 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -860,6 +860,24 @@ impl User { } Ok(()) } + + pub async fn find_by_device_id<'e, E>(executor: E, device_id: i64) -> Result, SqlxError> + where + E: PgExecutor<'e>, + { + query_as!( + Self, + "SELECT u.id \"id?\", u.username, u.password_hash, u.last_name, u.first_name, u.email, \ + u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, \ + u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_login \ + FROM \"user\" as u \ + JOIN \"device\" as d ON u.id = d.user_id \ + WHERE d.id = $1", + device_id + ) + .fetch_optional(executor) + .await + } } #[cfg(test)] diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index 9accb10acc..fe088ea5c3 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -1,8 +1,5 @@ use crate::{ - db::{ - models::enrollment::{Token, POLLING_TOKEN_TYPE}, - DbPool, - }, + db::{models::polling_token::PollingToken, DbPool, Device}, enterprise::license::{get_cached_license, validate_license}, grpc::utils::build_device_config_response, }; @@ -20,9 +17,9 @@ impl PollingServer { Self { pool } } - /// Checks if token provided with request corresponds to a valid auth session - async fn validate_session(&self, token: &str) -> Result { - debug!("Validating auth session. Token: {token}"); + /// Checks validity of polling session + async fn validate_session(&self, token: &str) -> Result { + debug!("Validating polling token. Token: {token}"); // Polling service is enterprise-only, check the lincense if validate_license(get_cached_license().as_ref()).is_err() { @@ -30,30 +27,32 @@ impl PollingServer { return Err(Status::permission_denied("no valid license")); } - let token = Token::find_by_id(&self.pool, token).await?; - debug!("Found matching token, verifying validity: {token:?}."); - // Auth tokens are valid indefinitely - if token - .token_type - .as_ref() - .is_some_and(|token_type| token_type == POLLING_TOKEN_TYPE) - { - Ok(token) - } else { - error!( - "Invalid token type used in polling process: {:?}", - token.token_type - ); - Err(Status::permission_denied("invalid token")) - } + // Validate the token + let Some(token) = PollingToken::find(&self.pool, token).await.map_err(|err| { + error!("Failed to retrieve token: {err}"); + Status::internal("failed to retrieve token") + })? else { + error!("Invalid token {token:?}"); + return Err(Status::permission_denied("invalid token")); + }; + + info!("Token validation successful {token:?}."); + Ok(token) } /// Prepares instance info for polling requests. Enterprise only. pub async fn info(&self, request: InstanceInfoRequest) -> Result { debug!("Getting network info for device: {:?}", request.pubkey); let token = self.validate_session(&request.token).await?; + let Some(device) = Device::find_by_id(&self.pool, token.device_id).await.map_err(|err| { + error!("Failed to retrieve device id {}: {err}", token.device_id); + Status::internal("failed to retrieve device") + })? else { + error!("Device id {} not found", token.device_id); + return Err(Status::internal("device not found")); + }; let device_config = - build_device_config_response(&self.pool, &token, &request.pubkey).await?; + build_device_config_response(&self.pool, &device.wireguard_pubkey).await?; Ok(InstanceInfoResponse { device_config: Some(device_config), }) diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 058117b89c..dd5f0e802f 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -469,8 +469,8 @@ impl EnrollmentServer { request: ExistingDevice, ) -> Result { debug!("Getting network info for device: {:?}", request.pubkey); - let token = self.validate_session(&request.token).await?; - build_device_config_response(&self.pool, &token, &request.pubkey).await + let _token = self.validate_session(&request.token).await?; + build_device_config_response(&self.pool, &request.pubkey).await } } diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index 0543d9f491..db95c2782e 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -4,18 +4,14 @@ use tonic::Status; use super::proto::{DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse}; use crate::db::{ - models::{device::WireguardNetworkDevice, enrollment::Token, wireguard::WireguardNetwork}, - DbPool, Device, Settings, + models::{device::WireguardNetworkDevice, wireguard::WireguardNetwork}, + DbPool, Device, Settings, User, }; pub(crate) async fn build_device_config_response( pool: &DbPool, - token: &Token, pubkey: &str, ) -> Result { - // get enrollment user - let user = token.fetch_user(pool).await?; - Device::validate_pubkey(pubkey).map_err(|_| { error!("Invalid pubkey {pubkey}"); Status::invalid_argument("invalid pubkey") @@ -25,7 +21,6 @@ pub(crate) async fn build_device_config_response( error!("Failed to get device by its pubkey: {pubkey}"); Status::internal("unexpected error") })?; - let settings = Settings::get_settings(pool).await.map_err(|_| { error!("Failed to get settings"); Status::internal("unexpected error") @@ -40,6 +35,13 @@ pub(crate) async fn build_device_config_response( let Some(device) = device else { return Err(Status::internal("device not found error")); }; + let user = User::find_by_id(pool, device.user_id).await.map_err(|_| { + error!("Failed to get user: {}", device.user_id); + Status::internal("unexpected error") + })?.ok_or_else(|| { + error!("User not found: {}", device.user_id); + Status::internal("unexpected error") + })?; for network in networks { let (Some(device_id), Some(network_id)) = (device.id, network.id) else { continue; From 7212c79fd89647e0deaee8ccd5b6b0fd47e779e1 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 13:49:45 +0200 Subject: [PATCH 15/20] Fix `PollingToken::created_at` --- src/db/models/polling_token.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/db/models/polling_token.rs b/src/db/models/polling_token.rs index 3227a26854..3235f6b05c 100644 --- a/src/db/models/polling_token.rs +++ b/src/db/models/polling_token.rs @@ -1,4 +1,4 @@ -use chrono::NaiveDateTime; +use chrono::{NaiveDateTime, Utc}; use model_derive::Model; use sqlx::{query_as, Error as SqlxError}; @@ -12,7 +12,7 @@ pub struct PollingToken { pub id: Option, pub token: String, pub device_id: i64, - pub created_at: Option, + pub created_at: NaiveDateTime, } impl PollingToken { @@ -21,7 +21,7 @@ impl PollingToken { id: None, device_id, token: gen_alphanumeric(32), - created_at: None, + created_at: Utc::now().naive_utc(), } } From fe9f6b19ebb917ccb01f35b9f645a02169220f07 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 14:17:54 +0200 Subject: [PATCH 16/20] Deny polling info for inactive users --- src/enterprise/grpc/polling.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index fe088ea5c3..acfe2bdc95 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -1,5 +1,5 @@ use crate::{ - db::{models::polling_token::PollingToken, DbPool, Device}, + db::{models::polling_token::PollingToken, DbPool, Device, User}, enterprise::license::{get_cached_license, validate_license}, grpc::utils::build_device_config_response, }; @@ -36,6 +36,7 @@ impl PollingServer { return Err(Status::permission_denied("invalid token")); }; + // Polling tokens are valid indefinitely info!("Token validation successful {token:?}."); Ok(token) } @@ -51,6 +52,22 @@ impl PollingServer { error!("Device id {} not found", token.device_id); return Err(Status::internal("device not found")); }; + + // Ensure user is active + let device_id = device.id.expect("missing device id"); + let Some(user) = User::find_by_device_id(&self.pool, device_id).await.map_err(|err| { + error!("Failed to retrieve user for device id {device_id}: {err}"); + Status::internal("failed to retrieve user") + })? else { + error!("User for device id {device_id} not found"); + return Err(Status::internal("user not found")); + }; + if !user.is_active { + warn!("Denying polling info for inactive user {}({:?})", user.username, user.id); + return Err(Status::permission_denied("user inactive")); + } + + // Build & return polling info let device_config = build_device_config_response(&self.pool, &device.wireguard_pubkey).await?; Ok(InstanceInfoResponse { From a3c7c5cd9ee6abd268e937c8df4b030e372d69a7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 14:24:29 +0200 Subject: [PATCH 17/20] Update protos --- proto | 2 +- src/enterprise/grpc/polling.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/proto b/proto index 6c89f195e6..57654ba89d 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 6c89f195e6d9de08a13c97ccf09550873a845d4f +Subproject commit 57654ba89d5a1675c0c2d6bac8f7910cb28257f5 diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index acfe2bdc95..551f7135c5 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -43,7 +43,7 @@ impl PollingServer { /// Prepares instance info for polling requests. Enterprise only. pub async fn info(&self, request: InstanceInfoRequest) -> Result { - debug!("Getting network info for device: {:?}", request.pubkey); + trace!("Polling info start"); let token = self.validate_session(&request.token).await?; let Some(device) = Device::find_by_id(&self.pool, token.device_id).await.map_err(|err| { error!("Failed to retrieve device id {}: {err}", token.device_id); @@ -52,6 +52,7 @@ impl PollingServer { error!("Device id {} not found", token.device_id); return Err(Status::internal("device not found")); }; + debug!("Polling info for device: {}", device.wireguard_pubkey); // Ensure user is active let device_id = device.id.expect("missing device id"); From 143a40af485162b9748d660e8be4bdb1e0db620c Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 14:50:55 +0200 Subject: [PATCH 18/20] Update protos --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 57654ba89d..d069a0e530 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 57654ba89d5a1675c0c2d6bac8f7910cb28257f5 +Subproject commit d069a0e5304281cfc8b09e949a8e7a9feb5fc115 From ab74ddc233b395b6a07f3ce1b87b4957fdabf105 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 14:58:47 +0200 Subject: [PATCH 19/20] Cargo fmt --- src/db/models/user.rs | 5 ++++- src/enterprise/grpc/polling.rs | 30 ++++++++++++++++++++---------- src/grpc/enrollment.rs | 15 +++++++-------- src/grpc/utils.rs | 17 ++++++++++------- 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index e0f5575bf7..6032543af8 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -872,7 +872,10 @@ impl User { Ok(()) } - pub async fn find_by_device_id<'e, E>(executor: E, device_id: i64) -> Result, SqlxError> + pub async fn find_by_device_id<'e, E>( + executor: E, + device_id: i64, + ) -> Result, SqlxError> where E: PgExecutor<'e>, { diff --git a/src/enterprise/grpc/polling.rs b/src/enterprise/grpc/polling.rs index 551f7135c5..f351a89be2 100644 --- a/src/enterprise/grpc/polling.rs +++ b/src/enterprise/grpc/polling.rs @@ -31,7 +31,8 @@ impl PollingServer { let Some(token) = PollingToken::find(&self.pool, token).await.map_err(|err| { error!("Failed to retrieve token: {err}"); Status::internal("failed to retrieve token") - })? else { + })? + else { error!("Invalid token {token:?}"); return Err(Status::permission_denied("invalid token")); }; @@ -45,10 +46,13 @@ impl PollingServer { pub async fn info(&self, request: InstanceInfoRequest) -> Result { trace!("Polling info start"); let token = self.validate_session(&request.token).await?; - let Some(device) = Device::find_by_id(&self.pool, token.device_id).await.map_err(|err| { - error!("Failed to retrieve device id {}: {err}", token.device_id); - Status::internal("failed to retrieve device") - })? else { + let Some(device) = Device::find_by_id(&self.pool, token.device_id) + .await + .map_err(|err| { + error!("Failed to retrieve device id {}: {err}", token.device_id); + Status::internal("failed to retrieve device") + })? + else { error!("Device id {} not found", token.device_id); return Err(Status::internal("device not found")); }; @@ -56,15 +60,21 @@ impl PollingServer { // Ensure user is active let device_id = device.id.expect("missing device id"); - let Some(user) = User::find_by_device_id(&self.pool, device_id).await.map_err(|err| { - error!("Failed to retrieve user for device id {device_id}: {err}"); - Status::internal("failed to retrieve user") - })? else { + let Some(user) = User::find_by_device_id(&self.pool, device_id) + .await + .map_err(|err| { + error!("Failed to retrieve user for device id {device_id}: {err}"); + Status::internal("failed to retrieve user") + })? + else { error!("User for device id {device_id} not found"); return Err(Status::internal("user not found")); }; if !user.is_active { - warn!("Denying polling info for inactive user {}({:?})", user.username, user.id); + warn!( + "Denying polling info for inactive user {}({:?})", + user.username, user.id + ); return Err(Status::permission_denied("user inactive")); } diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 5a394965f1..7568fa629b 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -16,12 +16,13 @@ use crate::{ db::{ models::{ device::{DeviceConfig, DeviceInfo}, - enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE}, polling_token::PollingToken, + enrollment::{Token, TokenError, ENROLLMENT_TOKEN_TYPE}, + polling_token::PollingToken, }, DbPool, Device, GatewayEvent, Settings, User, }, - grpc::utils::build_device_config_response, enterprise::db::models::enterprise_settings::EnterpriseSettings, + grpc::utils::build_device_config_response, handlers::{mail::send_new_device_added_email, user::check_password_strength}, headers::get_device_info, ldap::utils::ldap_add_user, @@ -423,12 +424,10 @@ impl EnrollmentServer { // create polling token for further client communication debug!("Creating polling token for further client communication"); - let mut token = PollingToken::new( - device.id.ok_or_else(|| { - error!("No device id"); - Status::internal("unexpected error") - })?, - ); + let mut token = PollingToken::new(device.id.ok_or_else(|| { + error!("No device id"); + Status::internal("unexpected error") + })?); token.save(&mut *transaction).await.map_err(|err| { error!("Failed to save PollingToken: {err}"); Status::internal("failed to save polling token") diff --git a/src/grpc/utils.rs b/src/grpc/utils.rs index db95c2782e..f19a1bb555 100644 --- a/src/grpc/utils.rs +++ b/src/grpc/utils.rs @@ -35,13 +35,16 @@ pub(crate) async fn build_device_config_response( let Some(device) = device else { return Err(Status::internal("device not found error")); }; - let user = User::find_by_id(pool, device.user_id).await.map_err(|_| { - error!("Failed to get user: {}", device.user_id); - Status::internal("unexpected error") - })?.ok_or_else(|| { - error!("User not found: {}", device.user_id); - Status::internal("unexpected error") - })?; + let user = User::find_by_id(pool, device.user_id) + .await + .map_err(|_| { + error!("Failed to get user: {}", device.user_id); + Status::internal("unexpected error") + })? + .ok_or_else(|| { + error!("User not found: {}", device.user_id); + Status::internal("unexpected error") + })?; for network in networks { let (Some(device_id), Some(network_id)) = (device.id, network.id) else { continue; From cce5d08d8115399f26d1dd99e034609908f4c0bd Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 6 Sep 2024 15:56:57 +0200 Subject: [PATCH 20/20] Sqlx fixtures --- ...cfb992f473271cfa5f4e2cb8d9c92c4fea3f8.json | 17 +++ ...aa5ef5a214dbb487981d9864f6469a04805c2.json | 40 ++++++ ...04523dc1c69fa0052089d341c41862fdc4425.json | 38 ++++++ ...18f79cc32d8013a8255a87c745f73ef08a2df.json | 40 ++++++ ...e80b362b9ca168bdc2a4f10db6157b93b53e1.json | 24 ++++ ...6292046c0be2a179424b57c16767dd1d8b212.json | 125 ++++++++++++++++++ ...f52e6a872f5e974f586ed4d6a8c1ceaed8223.json | 14 ++ 7 files changed, 298 insertions(+) create mode 100644 .sqlx/query-0f00cd80f489fe50957305cd0e2cfb992f473271cfa5f4e2cb8d9c92c4fea3f8.json create mode 100644 .sqlx/query-5267ddfcbe18a9db34a0bc9f51baa5ef5a214dbb487981d9864f6469a04805c2.json create mode 100644 .sqlx/query-65af2457c30994d33ef2d6d265e04523dc1c69fa0052089d341c41862fdc4425.json create mode 100644 .sqlx/query-750c3343a64d8f02d6952ab115318f79cc32d8013a8255a87c745f73ef08a2df.json create mode 100644 .sqlx/query-838909479f3e9a1538980e1299be80b362b9ca168bdc2a4f10db6157b93b53e1.json create mode 100644 .sqlx/query-891d8d88e0eaba3d67a23dec7de6292046c0be2a179424b57c16767dd1d8b212.json create mode 100644 .sqlx/query-fac46fc03161bac460c30d9b61af52e6a872f5e974f586ed4d6a8c1ceaed8223.json diff --git a/.sqlx/query-0f00cd80f489fe50957305cd0e2cfb992f473271cfa5f4e2cb8d9c92c4fea3f8.json b/.sqlx/query-0f00cd80f489fe50957305cd0e2cfb992f473271cfa5f4e2cb8d9c92c4fea3f8.json new file mode 100644 index 0000000000..c386fd3d79 --- /dev/null +++ b/.sqlx/query-0f00cd80f489fe50957305cd0e2cfb992f473271cfa5f4e2cb8d9c92c4fea3f8.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"pollingtoken\" SET \"token\" = $2,\"device_id\" = $3,\"created_at\" = $4 WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Text", + "Int8", + "Timestamp" + ] + }, + "nullable": [] + }, + "hash": "0f00cd80f489fe50957305cd0e2cfb992f473271cfa5f4e2cb8d9c92c4fea3f8" +} diff --git a/.sqlx/query-5267ddfcbe18a9db34a0bc9f51baa5ef5a214dbb487981d9864f6469a04805c2.json b/.sqlx/query-5267ddfcbe18a9db34a0bc9f51baa5ef5a214dbb487981d9864f6469a04805c2.json new file mode 100644 index 0000000000..ebc7341f4e --- /dev/null +++ b/.sqlx/query-5267ddfcbe18a9db34a0bc9f51baa5ef5a214dbb487981d9864f6469a04805c2.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"token\",\"device_id\",\"created_at\" FROM \"pollingtoken\" WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "5267ddfcbe18a9db34a0bc9f51baa5ef5a214dbb487981d9864f6469a04805c2" +} diff --git a/.sqlx/query-65af2457c30994d33ef2d6d265e04523dc1c69fa0052089d341c41862fdc4425.json b/.sqlx/query-65af2457c30994d33ef2d6d265e04523dc1c69fa0052089d341c41862fdc4425.json new file mode 100644 index 0000000000..0111f9c656 --- /dev/null +++ b/.sqlx/query-65af2457c30994d33ef2d6d265e04523dc1c69fa0052089d341c41862fdc4425.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id \"id?\", \"token\",\"device_id\",\"created_at\" FROM \"pollingtoken\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "65af2457c30994d33ef2d6d265e04523dc1c69fa0052089d341c41862fdc4425" +} diff --git a/.sqlx/query-750c3343a64d8f02d6952ab115318f79cc32d8013a8255a87c745f73ef08a2df.json b/.sqlx/query-750c3343a64d8f02d6952ab115318f79cc32d8013a8255a87c745f73ef08a2df.json new file mode 100644 index 0000000000..ee81bee7e4 --- /dev/null +++ b/.sqlx/query-750c3343a64d8f02d6952ab115318f79cc32d8013a8255a87c745f73ef08a2df.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, token, device_id, created_at\n FROM pollingtoken WHERE token = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "token", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "device_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamp" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "750c3343a64d8f02d6952ab115318f79cc32d8013a8255a87c745f73ef08a2df" +} diff --git a/.sqlx/query-838909479f3e9a1538980e1299be80b362b9ca168bdc2a4f10db6157b93b53e1.json b/.sqlx/query-838909479f3e9a1538980e1299be80b362b9ca168bdc2a4f10db6157b93b53e1.json new file mode 100644 index 0000000000..f539daaee7 --- /dev/null +++ b/.sqlx/query-838909479f3e9a1538980e1299be80b362b9ca168bdc2a4f10db6157b93b53e1.json @@ -0,0 +1,24 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"pollingtoken\" (\"token\",\"device_id\",\"created_at\") VALUES ($1,$2,$3) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text", + "Int8", + "Timestamp" + ] + }, + "nullable": [ + false + ] + }, + "hash": "838909479f3e9a1538980e1299be80b362b9ca168bdc2a4f10db6157b93b53e1" +} diff --git a/.sqlx/query-891d8d88e0eaba3d67a23dec7de6292046c0be2a179424b57c16767dd1d8b212.json b/.sqlx/query-891d8d88e0eaba3d67a23dec7de6292046c0be2a179424b57c16767dd1d8b212.json new file mode 100644 index 0000000000..0f55514a6e --- /dev/null +++ b/.sqlx/query-891d8d88e0eaba3d67a23dec7de6292046c0be2a179424b57c16767dd1d8b212.json @@ -0,0 +1,125 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT u.id \"id?\", u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_login FROM \"user\" as u JOIN \"device\" as d ON u.id = d.user_id WHERE d.id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id?", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "last_name", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "first_name", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "phone", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "totp_enabled", + "type_info": "Bool" + }, + { + "ordinal": 9, + "name": "email_mfa_enabled", + "type_info": "Bool" + }, + { + "ordinal": 10, + "name": "totp_secret", + "type_info": "Bytea" + }, + { + "ordinal": 11, + "name": "email_mfa_secret", + "type_info": "Bytea" + }, + { + "ordinal": 12, + "name": "mfa_method: _", + "type_info": { + "Custom": { + "name": "mfa_method", + "kind": { + "Enum": [ + "none", + "one_time_password", + "webauthn", + "web3", + "email" + ] + } + } + } + }, + { + "ordinal": 13, + "name": "recovery_codes", + "type_info": "TextArray" + }, + { + "ordinal": 14, + "name": "is_active", + "type_info": "Bool" + }, + { + "ordinal": 15, + "name": "openid_login", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + false + ] + }, + "hash": "891d8d88e0eaba3d67a23dec7de6292046c0be2a179424b57c16767dd1d8b212" +} diff --git a/.sqlx/query-fac46fc03161bac460c30d9b61af52e6a872f5e974f586ed4d6a8c1ceaed8223.json b/.sqlx/query-fac46fc03161bac460c30d9b61af52e6a872f5e974f586ed4d6a8c1ceaed8223.json new file mode 100644 index 0000000000..7af831b096 --- /dev/null +++ b/.sqlx/query-fac46fc03161bac460c30d9b61af52e6a872f5e974f586ed4d6a8c1ceaed8223.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM \"pollingtoken\" WHERE id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "fac46fc03161bac460c30d9b61af52e6a872f5e974f586ed4d6a8c1ceaed8223" +}