Skip to content

Commit 2a348dc

Browse files
wojcik91moubctez
andauthored
add Windows provisioning script to MSI installer (#625)
* add initial script to fetch provisioning config from entra ID * update naming and return values * fetch provisioning config from AD extension attributes * update docstring * read data from a single extension attribute * pass extension attribute as argument * us an arbitrary AD attribute * remove unnecessary escape chars * run script during MSI install * add MSI properties to control script execution * test new MSI build * remove silent mode switch * fix provisioning fragment * restore upgrade code * add provisioning script logging * rename provisioning config file * ensure transcript stop * don't restore legacy file * simplify provisioning config file in PS script * restore script resources * try to sidestep windows encoding issues * strip BOM manually * test new MSI images * setup MSI banners * move DB migrations execution * change default AD attribute * restore release workflow * Update src-tauri/resources-windows/scripts/Get-ProvisioningConfig.ps1 Co-authored-by: Adam <[email protected]> * restore EOF newlines --------- Co-authored-by: Adam <[email protected]>
1 parent 344b65d commit 2a348dc

File tree

10 files changed

+379
-19
lines changed

10 files changed

+379
-19
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
3+
<Fragment>
4+
<!-- Component to include the PowerShell script -->
5+
<Component Id="ProvisioningScriptFragment" Directory="INSTALLDIR" Guid="*">
6+
<File Id="GetProvisioningConfigScript" Source="..\..\resources-windows\scripts\Get-ProvisioningConfig.ps1" KeyPath="yes"/>
7+
</Component>
8+
9+
<!-- Define public properties that can be passed to the MSI -->
10+
<Property Id="PROVISIONING" Secure="yes"/>
11+
<Property Id="ADATTRIBUTE" Value="defguardProvisioningConfig"/>
12+
13+
<!-- Custom action to run the PowerShell script -->
14+
<CustomAction Id="RunProvisioningScript"
15+
Directory="INSTALLDIR"
16+
Execute="deferred"
17+
Impersonate="yes"
18+
Return="check"
19+
ExeCommand="powershell.exe -ExecutionPolicy Bypass -File &quot;[INSTALLDIR]Get-ProvisioningConfig.ps1&quot; -ADAttribute &quot;[ADATTRIBUTE]&quot;"/>
20+
21+
<!-- Schedule the custom action to run only if PROVISIONING property is set -->
22+
<InstallExecuteSequence>
23+
<Custom Action="RunProvisioningScript" After="InstallFiles">PROVISIONING AND NOT REMOVE</Custom>
24+
</InstallExecuteSequence>
25+
</Fragment>
26+
</Wix>

src-tauri/resources-windows/service-fragment.wxs renamed to src-tauri/resources-windows/fragments/service.wxs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
33
<Fragment>
44
<DirectoryRef Id="INSTALLDIR">
5-
<Component Id="DefGuardServiceFragment">
5+
<Component Id="DefguardServiceFragment">
66
<File KeyPath="yes" Id="DefguardServiceFile" Source="..\..\defguard-service.exe" />
77
<ServiceInstall
88
Account="LocalSystem"
90 KB
Loading
14 KB
Loading
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
#Requires -Version 5.1
2+
3+
<#
4+
.SYNOPSIS
5+
Retrieves Defguard client provisioning configuration for the currently logged-in user from Active Directory or Entra ID.
6+
7+
.DESCRIPTION
8+
This script detects whether the computer is joined to on-premises Active Directory
9+
or Entra ID (Azure AD), then fetches Defguard provisioning data (URL and enrollment token) from the appropriate source.
10+
- On-premises AD: Reads from specified attribute (default: defguardProvisioningConfig)
11+
- Entra ID: Reads from custom security attributes under the 'Defguard' set
12+
- Workgroup: Exits gracefully
13+
The retrieved enrollment data is saved to a JSON file for the Defguard client to use.
14+
15+
.PARAMETER ADAttribute
16+
Specifies which Active Directory attribute to read from (default: defguardProvisioningConfig)
17+
#>
18+
19+
param(
20+
[string]$ADAttribute = "defguardProvisioningConfig"
21+
)
22+
23+
# Check device join status
24+
function Get-DomainJoinStatus {
25+
try {
26+
$computerSystem = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop
27+
28+
# Check for traditional domain join
29+
if ($computerSystem.PartOfDomain -eq $true) {
30+
return @{
31+
JoinType = "OnPremisesAD"
32+
Domain = $computerSystem.Domain
33+
}
34+
}
35+
36+
# Check for Entra ID (Azure AD) join
37+
$dsregStatus = dsregcmd /status
38+
if ($dsregStatus -match "AzureAdJoined\s*:\s*YES") {
39+
$tenantName = ($dsregStatus | Select-String "TenantName\s*:\s*(.+)").Matches.Groups[1].Value.Trim()
40+
return @{
41+
JoinType = "EntraID"
42+
Domain = $tenantName
43+
}
44+
}
45+
46+
# Check for Hybrid join
47+
if ($dsregStatus -match "DomainJoined\s*:\s*YES" -and $dsregStatus -match "AzureAdJoined\s*:\s*YES") {
48+
return @{
49+
JoinType = "Hybrid"
50+
Domain = $computerSystem.Domain
51+
}
52+
}
53+
54+
# Not joined to any directory
55+
return @{
56+
JoinType = "Workgroup"
57+
Domain = $null
58+
}
59+
60+
} catch {
61+
Write-Host "Unable to determine domain status: $_" -ForegroundColor Yellow
62+
return @{
63+
JoinType = "Unknown"
64+
Domain = $null
65+
}
66+
}
67+
}
68+
69+
# Save Defguard enrollment data to JSON
70+
function Save-DefguardEnrollmentData {
71+
param(
72+
[string]$EnrollmentUrl,
73+
[string]$EnrollmentToken
74+
)
75+
76+
# Create Defguard directory in AppData\Roaming
77+
$defguardDir = Join-Path $env:APPDATA "net.defguard"
78+
$jsonOutputPath = Join-Path $defguardDir "provisioning.json"
79+
80+
try {
81+
# Create directory if it doesn't exist
82+
if (-not (Test-Path -Path $defguardDir)) {
83+
New-Item -ItemType Directory -Path $defguardDir -Force | Out-Null
84+
Write-Host "`nCreated directory: $defguardDir" -ForegroundColor Gray
85+
}
86+
87+
$jsonData = @{
88+
enrollment_url = $EnrollmentUrl
89+
enrollment_token = $EnrollmentToken
90+
}
91+
92+
$jsonData | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonOutputPath -Encoding UTF8 -Force
93+
Write-Host "`nDefguard enrollment data saved to: $jsonOutputPath" -ForegroundColor Green
94+
return $true
95+
} catch {
96+
Write-Host "`nFailed to save JSON file: $_" -ForegroundColor Red
97+
return $false
98+
}
99+
}
100+
101+
# Get Defguard client provisioning config from on-premises AD
102+
function Get-OnPremisesADProvisioningConfig {
103+
param(
104+
[string]$Username,
105+
[string]$ADAttribute
106+
)
107+
108+
# Check if Active Directory module is available
109+
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
110+
Write-Host "Active Directory module is not installed. Please install RSAT tools." -ForegroundColor Red
111+
return
112+
}
113+
114+
# Import the Active Directory module
115+
try {
116+
Import-Module ActiveDirectory -ErrorAction Stop
117+
} catch {
118+
Write-Host "Failed to import Active Directory module: $_" -ForegroundColor Red
119+
return
120+
}
121+
122+
# Fetch AD user information
123+
try {
124+
$adUser = Get-ADUser -Identity $Username -Properties * -ErrorAction Stop
125+
126+
# Display user information
127+
Write-Host "`n=== On-Premises Active Directory User Information ===" -ForegroundColor Cyan
128+
Write-Host "Display Name: $($adUser.DisplayName)"
129+
Write-Host "Username (SAM): $($adUser.SamAccountName)"
130+
Write-Host "User Principal Name: $($adUser.UserPrincipalName)"
131+
Write-Host "Email: $($adUser.EmailAddress)"
132+
Write-Host "Enabled: $($adUser.Enabled)"
133+
Write-Host "Created: $($adUser.Created)"
134+
Write-Host "Distinguished Name: $($adUser.DistinguishedName)"
135+
Write-Host "======================================================`n" -ForegroundColor Cyan
136+
137+
# Check for Defguard enrollment data in the specified AD attribute
138+
Write-Host "`n--- Active Directory Attribute ---" -ForegroundColor Yellow
139+
140+
# Read JSON data from the specified AD attribute
141+
$jsonData = $adUser.$ADAttribute
142+
143+
Write-Host "Defguard Enrollment JSON ($ADAttribute): $jsonData"
144+
145+
if ($jsonData) {
146+
try {
147+
# Parse the JSON data
148+
$enrollmentConfig = $jsonData | ConvertFrom-Json -ErrorAction Stop
149+
150+
# Extract URL and token from the parsed JSON
151+
$enrollmentUrl = $enrollmentConfig.enrollmentUrl
152+
$enrollmentToken = $enrollmentConfig.enrollmentToken
153+
154+
Write-Host "Defguard Enrollment URL: $enrollmentUrl"
155+
Write-Host "Defguard Enrollment Token: $enrollmentToken"
156+
157+
# Save enrollment data to JSON file only if both URL and token exist
158+
if ($enrollmentUrl -and $enrollmentToken) {
159+
Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl `
160+
-EnrollmentToken $enrollmentToken
161+
} else {
162+
Write-Host "`nWarning: Incomplete Defguard enrollment data in JSON. Both URL and token are required." -ForegroundColor Yellow
163+
}
164+
} catch {
165+
Write-Host "Failed to parse JSON from AD attribute '$ADAttribute': $_" -ForegroundColor Red
166+
Write-Host "JSON data should be in format: {`"enrollmentUrl`":`"https://...`",`"enrollmentToken`":`"token-value`"}" -ForegroundColor Yellow
167+
}
168+
} else {
169+
Write-Host "No Defguard enrollment data found in the specified AD attribute." -ForegroundColor Yellow
170+
}
171+
172+
Write-Host "======================================================`n" -ForegroundColor Cyan
173+
174+
175+
return
176+
177+
} catch {
178+
Write-Host "Failed to retrieve AD user information for '$Username': $_" -ForegroundColor Red
179+
return
180+
}
181+
}
182+
183+
# Get Defguard client provisioning config from Entra ID
184+
function Get-EntraIDProvisioningConfig {
185+
# Check if Microsoft.Graph module is available
186+
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) {
187+
Write-Host "Microsoft.Graph.Users module is not installed." -ForegroundColor Yellow
188+
Write-Host "Install it with: Install-Module Microsoft.Graph.Users -Scope CurrentUser" -ForegroundColor Yellow
189+
return
190+
}
191+
192+
# Import the module
193+
try {
194+
Import-Module Microsoft.Graph.Users -ErrorAction Stop
195+
} catch {
196+
Write-Host "Failed to import Microsoft.Graph.Users module: $_" -ForegroundColor Red
197+
return
198+
}
199+
200+
# Connect to Microsoft Graph
201+
try {
202+
$context = Get-MgContext -ErrorAction SilentlyContinue
203+
204+
if (-not $context) {
205+
Write-Host "Connecting to Microsoft Graph (authentication required)..." -ForegroundColor Yellow
206+
Write-Host "Note: Requesting additional permissions for custom security attributes..." -ForegroundColor Gray
207+
Connect-MgGraph -Scopes "User.Read", "CustomSecAttributeAssignment.Read.All" -ErrorAction Stop
208+
} else {
209+
# Check if we have the required scope for custom attributes
210+
$hasCustomAttrScope = $context.Scopes -contains "CustomSecAttributeAssignment.Read.All"
211+
if (-not $hasCustomAttrScope) {
212+
Write-Host "Warning: Missing 'CustomSecAttributeAssignment.Read.All' permission." -ForegroundColor Yellow
213+
Write-Host "Custom security attributes will not be available. Reconnect with:" -ForegroundColor Yellow
214+
Write-Host " Connect-MgGraph -Scopes 'User.Read', 'CustomSecAttributeAssignment.Read.All'" -ForegroundColor Gray
215+
return
216+
}
217+
}
218+
219+
# Get current user info including custom security attributes
220+
$properties = @(
221+
"DisplayName",
222+
"UserPrincipalName",
223+
"Mail",
224+
"AccountEnabled",
225+
"CreatedDateTime",
226+
"Id",
227+
"CustomSecurityAttributes"
228+
)
229+
230+
$mgUser = Get-MgUser -UserId (Get-MgContext).Account -Property $properties -ErrorAction Stop
231+
232+
# Display user information
233+
Write-Host "`n=== Entra ID (Azure AD) User Information ===" -ForegroundColor Cyan
234+
Write-Host "Display Name: $($mgUser.DisplayName)"
235+
Write-Host "User Principal Name: $($mgUser.UserPrincipalName)"
236+
Write-Host "Email: $($mgUser.Mail)"
237+
Write-Host "Account Enabled: $($mgUser.AccountEnabled)"
238+
Write-Host "Created: $($mgUser.CreatedDateTime)"
239+
Write-Host "User ID: $($mgUser.Id)"
240+
241+
# Try to get custom security attributes
242+
if ($mgUser.CustomSecurityAttributes) {
243+
Write-Host "`n--- Custom Security Attributes ---" -ForegroundColor Yellow
244+
245+
# Access Defguard attributes
246+
if ($mgUser.CustomSecurityAttributes.AdditionalProperties) {
247+
$defguardAttrs = $mgUser.CustomSecurityAttributes.AdditionalProperties["Defguard"]
248+
249+
if ($defguardAttrs) {
250+
$enrollmentUrl = $defguardAttrs["EnrollmentUrl"]
251+
$enrollmentToken = $defguardAttrs["EnrollmentToken"]
252+
253+
Write-Host "Defguard Enrollment URL: $enrollmentUrl"
254+
Write-Host "Defguard Enrollment Token: $enrollmentToken"
255+
256+
# Save enrollment data to JSON file only if both URL and token exist
257+
if ($enrollmentUrl -and $enrollmentToken) {
258+
Save-DefguardEnrollmentData -EnrollmentUrl $enrollmentUrl `
259+
-EnrollmentToken $enrollmentToken
260+
} else {
261+
Write-Host "`nWarning: Incomplete Defguard enrollment data. Both URL and token are required." -ForegroundColor Yellow
262+
}
263+
} else {
264+
Write-Host "No Defguard attributes found for this user." -ForegroundColor Gray
265+
}
266+
} else {
267+
Write-Host "No custom security attributes found." -ForegroundColor Gray
268+
}
269+
} else {
270+
Write-Host "`nCustom security attributes not available." -ForegroundColor Gray
271+
Write-Host "(May require additional permissions or attributes not set)" -ForegroundColor Gray
272+
}
273+
274+
Write-Host "=============================================`n" -ForegroundColor Cyan
275+
276+
} catch {
277+
Write-Host "Failed to retrieve Entra ID user information: $_" -ForegroundColor Red
278+
Write-Host "Error details: $($_.Exception.Message)" -ForegroundColor Red
279+
}
280+
}
281+
282+
# Log all script output to file
283+
$defguardDir = Join-Path $env:APPDATA "net.defguard"
284+
$logFilePath = Join-Path $defguardDir "provisioning_log.txt"
285+
Start-Transcript -Path $logFilePath
286+
287+
# Main script execution
288+
Write-Host "Detecting domain join status..." -ForegroundColor Gray
289+
290+
$joinStatus = Get-DomainJoinStatus
291+
$joinType = $joinStatus.JoinType
292+
293+
Write-Host "Join Type = '$joinType'" -ForegroundColor Magenta
294+
295+
if ($joinType -eq "OnPremisesAD") {
296+
Write-Host "Connected to on-premises Active Directory: $($joinStatus.Domain)" -ForegroundColor Green
297+
$currentUser = $env:USERNAME
298+
Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute
299+
} elseif ($joinType -eq "Hybrid") {
300+
Write-Host "Hybrid join detected (both on-premises AD and Entra ID): $($joinStatus.Domain)" -ForegroundColor Green
301+
Write-Host "Querying on-premises Active Directory..." -ForegroundColor Gray
302+
$currentUser = $env:USERNAME
303+
Get-OnPremisesADProvisioningConfig -Username $currentUser -ADAttribute $ADAttribute
304+
} elseif ($joinType -eq "EntraID") {
305+
Write-Host "Connected to Entra ID (Azure AD)" -ForegroundColor Green
306+
if ($joinStatus.Domain) {
307+
Write-Host " Tenant: $($joinStatus.Domain)" -ForegroundColor Gray
308+
}
309+
Get-EntraIDProvisioningConfig
310+
} elseif ($joinType -eq "Workgroup") {
311+
Write-Host "This computer is not connected to a domain (Workgroup). Exiting." -ForegroundColor Yellow
312+
} else {
313+
Write-Host "Unable to determine domain connection status. Exiting." -ForegroundColor Yellow
314+
}
315+
316+
Stop-Transcript

src-tauri/src/bin/defguard-client.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use defguard_client::{
1515
appstate::AppState,
1616
commands::*,
1717
database::{
18+
handle_db_migrations,
1819
models::{location_stats::LocationStats, tunnel::TunnelStats},
1920
DB_POOL,
2021
},
@@ -39,14 +40,6 @@ const LOGGING_TARGET_IGNORE_LIST: [&str; 5] = ["tauri", "sqlx", "hyper", "h2", "
3940
static LOG_INCLUDES: LazyLock<Vec<String>> = LazyLock::new(load_log_targets);
4041

4142
async fn startup(app_handle: &AppHandle) {
42-
debug!("Running database migrations, if there are any.");
43-
sqlx::migrate!()
44-
.run(&*DB_POOL)
45-
.await
46-
.expect("Failed to apply database migrations.");
47-
debug!("Applied all database migrations that were pending. If any.");
48-
debug!("Database setup has been completed successfully.");
49-
5043
debug!("Purging old stats from the database.");
5144
if let Err(err) = LocationStats::purge(&*DB_POOL).await {
5245
error!("Failed to purge location stats: {err}");
@@ -246,6 +239,9 @@ fn main() {
246239
.build(),
247240
)?;
248241

242+
// run DB migrations
243+
tauri::async_runtime::block_on(handle_db_migrations());
244+
249245
// Check if client needs to be initialized
250246
// and try to load provisioning config if necessary
251247
let provisioning_config =

src-tauri/src/database/mod.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,13 @@ fn prepare_db_url() -> Result<String, Error> {
9393
))
9494
}
9595
}
96+
97+
pub async fn handle_db_migrations() {
98+
debug!("Running database migrations, if there are any.");
99+
sqlx::migrate!()
100+
.run(&*DB_POOL)
101+
.await
102+
.expect("Failed to apply database migrations.");
103+
debug!("Applied all database migrations that were pending. If any.");
104+
debug!("Database setup has been completed successfully.");
105+
}

0 commit comments

Comments
 (0)