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/*"
]
}