diff --git a/src-tauri/resources-windows/fragments/provisioning.wxs b/src-tauri/resources-windows/fragments/provisioning.wxs new file mode 100644 index 00000000..06f993a4 --- /dev/null +++ b/src-tauri/resources-windows/fragments/provisioning.wxs @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + PROVISIONING AND NOT REMOVE + + + diff --git a/src-tauri/resources-windows/service-fragment.wxs b/src-tauri/resources-windows/fragments/service.wxs similarity index 95% rename from src-tauri/resources-windows/service-fragment.wxs rename to src-tauri/resources-windows/fragments/service.wxs index 1c16bda7..835720c5 100644 --- a/src-tauri/resources-windows/service-fragment.wxs +++ b/src-tauri/resources-windows/fragments/service.wxs @@ -2,7 +2,7 @@ - + + +param( + [string]$ADAttribute = "defguardProvisioningConfig" +) + +# Check device join status +function Get-DomainJoinStatus { + try { + $computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop + + # Check for traditional domain join + if ($computerSystem.PartOfDomain -eq $true) { + return @{ + JoinType = "OnPremisesAD" + Domain = $computerSystem.Domain + } + } + + # Check for Entra ID (Azure AD) join + $dsregStatus = dsregcmd /status + if ($dsregStatus -match "AzureAdJoined\s*:\s*YES") { + $tenantName = ($dsregStatus | Select-String "TenantName\s*:\s*(.+)").Matches.Groups[1].Value.Trim() + return @{ + JoinType = "EntraID" + Domain = $tenantName + } + } + + # Check for Hybrid join + if ($dsregStatus -match "DomainJoined\s*:\s*YES" -and $dsregStatus -match "AzureAdJoined\s*:\s*YES") { + return @{ + JoinType = "Hybrid" + Domain = $computerSystem.Domain + } + } + + # Not joined to any directory + return @{ + JoinType = "Workgroup" + Domain = $null + } + + } catch { + Write-Host "Unable to determine domain status: $_" -ForegroundColor Yellow + return @{ + JoinType = "Unknown" + Domain = $null + } + } +} + +# Save Defguard enrollment data to JSON +function Save-DefguardEnrollmentData { + param( + [string]$EnrollmentUrl, + [string]$EnrollmentToken + ) + + # Create Defguard directory in AppData\Roaming + $defguardDir = Join-Path $env:APPDATA "net.defguard" + $jsonOutputPath = Join-Path $defguardDir "provisioning.json" + + try { + # Create directory if it doesn't exist + if (-not (Test-Path -Path $defguardDir)) { + New-Item -ItemType Directory -Path $defguardDir -Force | Out-Null + Write-Host "`nCreated directory: $defguardDir" -ForegroundColor Gray + } + + $jsonData = @{ + enrollment_url = $EnrollmentUrl + enrollment_token = $EnrollmentToken + } + + $jsonData | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonOutputPath -Encoding UTF8 -Force + Write-Host "`nDefguard enrollment data saved to: $jsonOutputPath" -ForegroundColor Green + return $true + } catch { + Write-Host "`nFailed to save JSON file: $_" -ForegroundColor Red + return $false + } +} + +# Get Defguard client provisioning config from on-premises AD +function Get-OnPremisesADProvisioningConfig { + param( + [string]$Username, + [string]$ADAttribute + ) + + # Check if Active Directory module is available + if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { + Write-Host "Active Directory module is not installed. Please install RSAT tools." -ForegroundColor Red + return + } + + # Import the Active Directory module + try { + Import-Module ActiveDirectory -ErrorAction Stop + } catch { + Write-Host "Failed to import Active Directory module: $_" -ForegroundColor Red + return + } + + # Fetch AD user information + try { + $adUser = Get-ADUser -Identity $Username -Properties * -ErrorAction Stop + + # Display user information + Write-Host "`n=== On-Premises Active Directory User Information ===" -ForegroundColor Cyan + Write-Host "Display Name: $($adUser.DisplayName)" + Write-Host "Username (SAM): $($adUser.SamAccountName)" + Write-Host "User Principal Name: $($adUser.UserPrincipalName)" + Write-Host "Email: $($adUser.EmailAddress)" + Write-Host "Enabled: $($adUser.Enabled)" + Write-Host "Created: $($adUser.Created)" + Write-Host "Distinguished Name: $($adUser.DistinguishedName)" + Write-Host "======================================================`n" -ForegroundColor Cyan + + # Check for Defguard enrollment data in the specified AD attribute + Write-Host "`n--- Active Directory Attribute ---" -ForegroundColor Yellow + + # Read JSON data from the specified AD attribute + $jsonData = $adUser.$ADAttribute + + Write-Host "Defguard Enrollment JSON ($ADAttribute): $jsonData" + + if ($jsonData) { + try { + # Parse the JSON data + $enrollmentConfig = $jsonData | ConvertFrom-Json -ErrorAction Stop + + # Extract URL and token from the parsed JSON + $enrollmentUrl = $enrollmentConfig.enrollmentUrl + $enrollmentToken = $enrollmentConfig.enrollmentToken + + Write-Host "Defguard Enrollment URL: $enrollmentUrl" + Write-Host "Defguard Enrollment Token: $enrollmentToken" + + # Save enrollment data to JSON file only if both URL and token exist + if ($enrollmentUrl -and $enrollmentToken) { + Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl ` + -EnrollmentToken $enrollmentToken + } else { + Write-Host "`nWarning: Incomplete Defguard enrollment data in JSON. Both URL and token are required." -ForegroundColor Yellow + } + } catch { + Write-Host "Failed to parse JSON from AD attribute '$ADAttribute': $_" -ForegroundColor Red + Write-Host "JSON data should be in format: {`"enrollmentUrl`":`"https://...`",`"enrollmentToken`":`"token-value`"}" -ForegroundColor Yellow + } + } else { + Write-Host "No Defguard enrollment data found in the specified AD attribute." -ForegroundColor Yellow + } + + Write-Host "======================================================`n" -ForegroundColor Cyan + + + return + + } catch { + Write-Host "Failed to retrieve AD user information for '$Username': $_" -ForegroundColor Red + return + } +} + +# Get Defguard client provisioning config from Entra ID +function Get-EntraIDProvisioningConfig { + # Check if Microsoft.Graph module is available + if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) { + Write-Host "Microsoft.Graph.Users module is not installed." -ForegroundColor Yellow + Write-Host "Install it with: Install-Module Microsoft.Graph.Users -Scope CurrentUser" -ForegroundColor Yellow + return + } + + # Import the module + try { + Import-Module Microsoft.Graph.Users -ErrorAction Stop + } catch { + Write-Host "Failed to import Microsoft.Graph.Users module: $_" -ForegroundColor Red + return + } + + # Connect to Microsoft Graph + try { + $context = Get-MgContext -ErrorAction SilentlyContinue + + if (-not $context) { + Write-Host "Connecting to Microsoft Graph (authentication required)..." -ForegroundColor Yellow + Write-Host "Note: Requesting additional permissions for custom security attributes..." -ForegroundColor Gray + Connect-MgGraph -Scopes "User.Read", "CustomSecAttributeAssignment.Read.All" -ErrorAction Stop + } else { + # Check if we have the required scope for custom attributes + $hasCustomAttrScope = $context.Scopes -contains "CustomSecAttributeAssignment.Read.All" + if (-not $hasCustomAttrScope) { + Write-Host "Warning: Missing 'CustomSecAttributeAssignment.Read.All' permission." -ForegroundColor Yellow + Write-Host "Custom security attributes will not be available. Reconnect with:" -ForegroundColor Yellow + Write-Host " Connect-MgGraph -Scopes 'User.Read', 'CustomSecAttributeAssignment.Read.All'" -ForegroundColor Gray + return + } + } + + # Get current user info including custom security attributes + $properties = @( + "DisplayName", + "UserPrincipalName", + "Mail", + "AccountEnabled", + "CreatedDateTime", + "Id", + "CustomSecurityAttributes" + ) + + $mgUser = Get-MgUser -UserId (Get-MgContext).Account -Property $properties -ErrorAction Stop + + # Display user information + Write-Host "`n=== Entra ID (Azure AD) User Information ===" -ForegroundColor Cyan + Write-Host "Display Name: $($mgUser.DisplayName)" + Write-Host "User Principal Name: $($mgUser.UserPrincipalName)" + Write-Host "Email: $($mgUser.Mail)" + Write-Host "Account Enabled: $($mgUser.AccountEnabled)" + Write-Host "Created: $($mgUser.CreatedDateTime)" + Write-Host "User ID: $($mgUser.Id)" + + # Try to get custom security attributes + if ($mgUser.CustomSecurityAttributes) { + Write-Host "`n--- Custom Security Attributes ---" -ForegroundColor Yellow + + # Access Defguard attributes + if ($mgUser.CustomSecurityAttributes.AdditionalProperties) { + $defguardAttrs = $mgUser.CustomSecurityAttributes.AdditionalProperties["Defguard"] + + if ($defguardAttrs) { + $enrollmentUrl = $defguardAttrs["EnrollmentUrl"] + $enrollmentToken = $defguardAttrs["EnrollmentToken"] + + Write-Host "Defguard Enrollment URL: $enrollmentUrl" + Write-Host "Defguard Enrollment Token: $enrollmentToken" + + # Save enrollment data to JSON file only if both URL and token exist + if ($enrollmentUrl -and $enrollmentToken) { + Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl ` + -EnrollmentToken $enrollmentToken + } else { + Write-Host "`nWarning: Incomplete Defguard enrollment data. Both URL and token are required." -ForegroundColor Yellow + } + } else { + Write-Host "No Defguard attributes found for this user." -ForegroundColor Gray + } + } else { + Write-Host "No custom security attributes found." -ForegroundColor Gray + } + } else { + Write-Host "`nCustom security attributes not available." -ForegroundColor Gray + Write-Host "(May require additional permissions or attributes not set)" -ForegroundColor Gray + } + + Write-Host "=============================================`n" -ForegroundColor Cyan + + } catch { + Write-Host "Failed to retrieve Entra ID user information: $_" -ForegroundColor Red + Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red + } +} + +# Log all script output to file +$defguardDir = Join-Path $env:APPDATA "net.defguard" +$logFilePath = Join-Path $defguardDir "provisioning_log.txt" +Start-Transcript -Path $logFilePath + +# Main script execution +Write-Host "Detecting domain join status..." -ForegroundColor Gray + +$joinStatus = Get-DomainJoinStatus +$joinType = $joinStatus.JoinType + +Write-Host "Join Type = '$joinType'" -ForegroundColor Magenta + +if ($joinType -eq "OnPremisesAD") { + Write-Host "Connected to on-premises Active Directory: $($joinStatus.Domain)" -ForegroundColor Green + $currentUser = $env:USERNAME + Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute +} elseif ($joinType -eq "Hybrid") { + Write-Host "Hybrid join detected (both on-premises AD and Entra ID): $($joinStatus.Domain)" -ForegroundColor Green + Write-Host "Querying on-premises Active Directory..." -ForegroundColor Gray + $currentUser = $env:USERNAME + Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute +} elseif ($joinType -eq "EntraID") { + Write-Host "Connected to Entra ID (Azure AD)" -ForegroundColor Green + if ($joinStatus.Domain) { + Write-Host " Tenant: $($joinStatus.Domain)" -ForegroundColor Gray + } + Get-EntraIDProvisioningConfig +} elseif ($joinType -eq "Workgroup") { + Write-Host "This computer is not connected to a domain (Workgroup). Exiting." -ForegroundColor Yellow +} else { + Write-Host "Unable to determine domain connection status. Exiting." -ForegroundColor Yellow +} + +Stop-Transcript diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs index 609cc4dc..03c36f1e 100644 --- a/src-tauri/src/bin/defguard-client.rs +++ b/src-tauri/src/bin/defguard-client.rs @@ -15,6 +15,7 @@ use defguard_client::{ appstate::AppState, commands::*, database::{ + handle_db_migrations, models::{location_stats::LocationStats, tunnel::TunnelStats}, DB_POOL, }, @@ -39,14 +40,6 @@ const LOGGING_TARGET_IGNORE_LIST: [&str; 5] = ["tauri", "sqlx", "hyper", "h2", " static LOG_INCLUDES: LazyLock> = LazyLock::new(load_log_targets); async fn startup(app_handle: &AppHandle) { - debug!("Running database migrations, if there are any."); - sqlx::migrate!() - .run(&*DB_POOL) - .await - .expect("Failed to apply database migrations."); - debug!("Applied all database migrations that were pending. If any."); - debug!("Database setup has been completed successfully."); - debug!("Purging old stats from the database."); if let Err(err) = LocationStats::purge(&*DB_POOL).await { error!("Failed to purge location stats: {err}"); @@ -246,6 +239,9 @@ fn main() { .build(), )?; + // run DB migrations + tauri::async_runtime::block_on(handle_db_migrations()); + // Check if client needs to be initialized // and try to load provisioning config if necessary let provisioning_config = diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index d6f59736..99866016 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -93,3 +93,13 @@ fn prepare_db_url() -> Result { )) } } + +pub async fn handle_db_migrations() { + debug!("Running database migrations, if there are any."); + sqlx::migrate!() + .run(&*DB_POOL) + .await + .expect("Failed to apply database migrations."); + debug!("Applied all database migrations that were pending. If any."); + debug!("Database setup has been completed successfully."); +} diff --git a/src-tauri/src/enterprise/provisioning/mod.rs b/src-tauri/src/enterprise/provisioning/mod.rs index 41cc7caf..a69e434b 100644 --- a/src-tauri/src/enterprise/provisioning/mod.rs +++ b/src-tauri/src/enterprise/provisioning/mod.rs @@ -1,11 +1,11 @@ -use std::{fs::OpenOptions, path::Path}; +use std::{fs, path::Path}; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; use crate::database::{models::instance::Instance, DB_POOL}; -const CONFIG_FILE_NAME: &str = "enrollment.json"; +const CONFIG_FILE_NAME: &str = "provisioning.json"; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ProvisioningConfig { @@ -16,14 +16,19 @@ pub struct ProvisioningConfig { impl ProvisioningConfig { /// Load configuration from a file at `path`. fn load(path: &Path) -> Option { - let file = match OpenOptions::new().read(true).open(path) { - Ok(file) => file, + // read content to string first to handle Windows encoding issues + let file_content = match fs::read_to_string(path) { + Ok(content) => content, Err(err) => { warn!("Failed to open provisioning configuration file at {path:?}. Error details: {err}"); return None; } }; - match serde_json::from_reader::<_, Self>(file) { + + // strip Windows BOM manually + let file_content = file_content.trim_start_matches('\u{FEFF}'); + + match serde_json::from_str::(file_content) { Ok(config) => Some(config), Err(err) => { warn!("Failed to parse provisioning configuration file at {path:?}. Error details: {err}"); @@ -57,8 +62,7 @@ pub async fn handle_client_initialization(app_handle: &AppHandle) -> Option { - info!("Provisioning config found in {data_dir:?}."); - debug!("Provisioning config: {config:?}"); + info!("Provisioning config found in {data_dir:?}: {config:?}"); return Some(config); } None => { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index fc4470f0..37c3ddfa 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -27,11 +27,16 @@ "digestAlgorithm": "sha256", "timestampUrl": "", "wix": { + "upgradeCode": "923b21f5-7d3f-4f5e-8dcb-43fe1c65fb43", + "bannerPath": "./resources-windows/msi/top_banner.png", + "dialogImagePath": "./resources-windows/msi/side_banner.png", "fragmentPaths": [ - "./resources-windows/service-fragment.wxs" + "./resources-windows/fragments/service.wxs", + "./resources-windows/fragments/provisioning.wxs" ], "componentRefs": [ - "DefGuardServiceFragment" + "DefguardServiceFragment", + "ProvisioningScriptFragment" ] } }, diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index 5027918d..826c27e7 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,8 +1,11 @@ { "bundle": { - "targets": ["msi"], + "targets": [ + "msi" + ], "resources": [ "resources-windows/binaries/*", + "resources-windows/scripts/*", "resources/icons/*" ] }