Skip to content

Commit 53309dc

Browse files
feat(agent-installer): require enrollment string, surface CA errors, add Agent name field
- Propagate AGENT_TUNNEL_* properties to deferred CA via Secure MSI Property declarations + explicit UsesProperties string. The deferred CA was previously seeing empty values because the wizard-set properties never crossed the UAC boundary. - Treat empty enrollment string as install failure (was silent skip). EnrollAgentTunnel CA now returns ActionResult.Failure and surfaces session.Message(InstallMessage.Error, ...) on the empty case and on enrollment timeout, non-zero exit, and exception paths. - Add optional Agent name field to AgentTunnelDialog. Resolution order at install time: dialog value > JWT jet_agent_name claim > computer name. Avoids "missing required --name" failures when the JWT lacks the claim. - Update Wizard.ShouldSkip-gated dialog so blank enrollment is blocked at UI validation (previously the dialog let users click Next on empty).
1 parent f105f7c commit 53309dc

8 files changed

Lines changed: 129 additions & 20 deletions

File tree

package/AgentWindowsManaged/Actions/AgentActions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,17 @@ internal static class AgentActions
289289
{
290290
Execute = Execute.deferred,
291291
Impersonate = false,
292+
// Deferred CAs only see properties bubbled through CustomActionData. The Set_<CA>_Props
293+
// immediate action expands [PROP] for each entry below before the deferred CA runs.
294+
UsesProperties = string.Join(";", new[]
295+
{
296+
AgentProperties.AgentTunnelEnrollmentString,
297+
AgentProperties.AgentTunnelGatewayUrl,
298+
AgentProperties.AgentTunnelAgentName,
299+
AgentProperties.AgentTunnelAdvertiseSubnets,
300+
AgentProperties.AgentTunnelAdvertiseDomains,
301+
AgentProperties.InstallDir,
302+
}.Select(p => $"{p}=[{p}]")),
292303
};
293304

294305
private static readonly ElevatedManagedAction registerExplorerCommand = new(

package/AgentWindowsManaged/Actions/CustomActions.cs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,20 @@ public static ActionResult EnrollAgentTunnel(Session session)
326326
string subnetsArg = session.Property(AgentProperties.AgentTunnelAdvertiseSubnets)?.Trim() ?? string.Empty;
327327
string domainsArg = session.Property(AgentProperties.AgentTunnelAdvertiseDomains)?.Trim() ?? string.Empty;
328328
string gatewayUrlArg = session.Property(AgentProperties.AgentTunnelGatewayUrl)?.Trim() ?? string.Empty;
329+
string agentNameArg = session.Property(AgentProperties.AgentTunnelAgentName)?.Trim() ?? string.Empty;
330+
331+
ActionResult Fail(string msg)
332+
{
333+
session.Log(msg);
334+
using Record record = new(0) { FormatString = msg };
335+
session.Message(InstallMessage.Error, record);
336+
return ActionResult.Failure;
337+
}
329338

330339
if (enrollmentString.Length == 0)
331340
{
332-
session.Log("Agent tunnel enrollment string not provided, skipping tunnel setup");
333-
return ActionResult.Success;
341+
return Fail("Agent tunnel feature was selected but no enrollment string was provided. " +
342+
"Paste a JWT from Devolutions Server, Hub, or Gateway, or deselect the Agent Tunnel feature.");
334343
}
335344

336345
try
@@ -342,8 +351,18 @@ public static ActionResult EnrollAgentTunnel(Session session)
342351
string installDir = session.Property(AgentProperties.InstallDir);
343352
string exePath = Path.Combine(installDir, Includes.EXECUTABLE_NAME);
344353

354+
// agent.exe `up` requires an agent name. Resolution: dialog value > JWT's
355+
// jet_agent_name (left to the agent CLI by omitting --name) > local computer name.
356+
string resolvedName = agentNameArg;
357+
if (resolvedName.Length == 0 && !JwtHasAgentName(enrollmentString))
358+
{
359+
resolvedName = Environment.MachineName;
360+
session.Log($"JWT carried no jet_agent_name and no name was provided in the wizard; falling back to computer name '{resolvedName}'");
361+
}
362+
345363
string arguments = $"up --enrollment-string \"{enrollmentString}\"";
346364
if (gatewayUrlArg.Length != 0) arguments += $" --gateway \"{gatewayUrlArg}\"";
365+
if (resolvedName.Length != 0) arguments += $" --name \"{resolvedName}\"";
347366
if (subnetsArg.Length != 0) arguments += $" --advertise-subnets \"{subnetsArg}\"";
348367

349368
string Redact(string s) => s.Replace(enrollmentString, "***");
@@ -362,8 +381,7 @@ public static ActionResult EnrollAgentTunnel(Session session)
362381
if (!process.WaitForExit(60_000))
363382
{
364383
try { process.Kill(); } catch { /* already gone */ }
365-
session.Log("Enrollment process timed out after 60 seconds");
366-
return ActionResult.Failure;
384+
return Fail("Agent tunnel enrollment timed out after 60 seconds.");
367385
}
368386
string stdout = process.StandardOutput.ReadToEnd();
369387
string stderr = process.StandardError.ReadToEnd();
@@ -373,8 +391,8 @@ public static ActionResult EnrollAgentTunnel(Session session)
373391

374392
if (process.ExitCode != 0)
375393
{
376-
session.Log($"Enrollment failed with exit code {process.ExitCode}");
377-
return ActionResult.Failure;
394+
string detail = !string.IsNullOrWhiteSpace(stderr) ? Redact(stderr).Trim() : $"exit code {process.ExitCode}";
395+
return Fail($"Agent tunnel enrollment failed: {detail}");
378396
}
379397

380398
if (domainsArg.Length != 0)
@@ -387,8 +405,25 @@ public static ActionResult EnrollAgentTunnel(Session session)
387405
}
388406
catch (Exception e)
389407
{
390-
session.Log($"Agent tunnel enrollment failed: {e}");
391-
return ActionResult.Failure;
408+
return Fail($"Agent tunnel enrollment failed: {e.Message}");
409+
}
410+
}
411+
412+
private static bool JwtHasAgentName(string jwt)
413+
{
414+
try
415+
{
416+
string[] parts = jwt.Split('.');
417+
if (parts.Length != 3) return false;
418+
string payload = parts[1].Replace('-', '+').Replace('_', '/');
419+
payload = payload.PadRight((payload.Length + 3) & ~3, '=');
420+
string json = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload));
421+
string name = JObject.Parse(json)["jet_agent_name"]?.ToString();
422+
return !string.IsNullOrWhiteSpace(name);
423+
}
424+
catch
425+
{
426+
return false;
392427
}
393428
}
394429

package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.Designer.cs

Lines changed: 51 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package/AgentWindowsManaged/Dialogs/AgentTunnelDialog.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public AgentTunnelDialog()
2222
public override bool ToProperties()
2323
{
2424
Runtime.Session[AgentProperties.AgentTunnelEnrollmentString] = enrollmentString.Text.Trim();
25+
Runtime.Session[AgentProperties.AgentTunnelAgentName] = agentName.Text.Trim();
2526
Runtime.Session[AgentProperties.AgentTunnelAdvertiseSubnets] = advertiseSubnets.Text.Trim();
2627
Runtime.Session[AgentProperties.AgentTunnelAdvertiseDomains] = advertiseDomains.Text.Trim();
2728
Runtime.Session[AgentProperties.AgentTunnelGatewayUrl] = gatewayUrl.Text.Trim();
@@ -34,6 +35,7 @@ public override void OnLoad(object sender, EventArgs e)
3435
banner.Image = Runtime.Session.GetResourceBitmap("WixUI_Bmp_Banner");
3536

3637
enrollmentString.Text = Runtime.Session.Property(AgentProperties.AgentTunnelEnrollmentString);
38+
agentName.Text = Runtime.Session.Property(AgentProperties.AgentTunnelAgentName);
3739
advertiseSubnets.Text = Runtime.Session.Property(AgentProperties.AgentTunnelAdvertiseSubnets);
3840
advertiseDomains.Text = Runtime.Session.Property(AgentProperties.AgentTunnelAdvertiseDomains);
3941
gatewayUrl.Text = Runtime.Session.Property(AgentProperties.AgentTunnelGatewayUrl);
@@ -43,10 +45,12 @@ public override void OnLoad(object sender, EventArgs e)
4345

4446
public override bool DoValidate()
4547
{
46-
// Tunnel is optional — if enrollment string is empty, skip tunnel setup entirely.
48+
// The dialog is only reached when the Agent Tunnel feature is selected (see Wizard.ShouldSkip),
49+
// so an enrollment string is required at this point.
4750
if (string.IsNullOrWhiteSpace(enrollmentString.Text))
4851
{
49-
return true;
52+
ShowValidationErrorString("Enrollment string is required. Paste a JWT from Devolutions Server, Hub, or Gateway, or go back and deselect the Agent Tunnel feature.");
53+
return false;
5054
}
5155

5256
// JWT shape: three base64url segments separated by dots. The agent's `up --enrollment-string`

package/AgentWindowsManaged/Program.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,14 @@ static void Main()
349349
// - Make DevolutionsDesktopAgent answer WM_CLOSE
350350
projectProperties.Add(new Property("MSIRESTARTMANAGERCONTROL", "Disable"));
351351

352+
// Agent tunnel properties: must be declared Secure so the values set in the wizard UI
353+
// survive the UAC boundary and reach the deferred CA via CustomActionData.
354+
projectProperties.Add(new Property(AgentProperties.AgentTunnelEnrollmentString, "") { Hidden = true, Secure = true });
355+
projectProperties.Add(new Property(AgentProperties.AgentTunnelGatewayUrl, "") { Secure = true });
356+
projectProperties.Add(new Property(AgentProperties.AgentTunnelAgentName, "") { Secure = true });
357+
projectProperties.Add(new Property(AgentProperties.AgentTunnelAdvertiseSubnets, "") { Secure = true });
358+
projectProperties.Add(new Property(AgentProperties.AgentTunnelAdvertiseDomains, "") { Secure = true });
359+
352360
project.Properties = projectProperties.ToArray();
353361
project.ManagedUI = new ManagedUI();
354362
project.ManagedUI.InstallDialogs.AddRange(Wizard.Dialogs);

package/AgentWindowsManaged/Properties/AgentProperties.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ internal partial class AgentProperties
3838
/// </summary>
3939
public static string AgentTunnelGatewayUrl = "AGENT_TUNNEL_GATEWAY_URL";
4040

41+
/// <summary>
42+
/// Optional agent display name. Resolution order at install time:
43+
/// dialog value (if non-empty) > JWT's jet_agent_name claim (if present) > local computer name.
44+
/// </summary>
45+
public static string AgentTunnelAgentName = "AGENT_TUNNEL_AGENT_NAME";
46+
4147
public AgentProperties(ISession runtimeSession)
4248
{
4349
this.runtimeSession = runtimeSession;

package/AgentWindowsManaged/Resources/DevolutionsAgent_en-us.wxl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ If it appears minimized then active it from the taskbar.</String>
6767
<String Id="AgentTunnelDlgSubnetsHint">Comma-separated CIDR notation, e.g. 10.10.0.0/24, 192.168.1.0/24. Leave blank for auto-detection.</String>
6868
<String Id="AgentTunnelDlgDomainsLabel">Advertise Domains:</String>
6969
<String Id="AgentTunnelDlgDomainsHint">Comma-separated DNS suffixes the agent can resolve, e.g. corp.example.com, lab.example.com. Leave blank to skip.</String>
70+
<String Id="AgentTunnelDlgAgentNameLabel">Agent name (optional):</String>
71+
<String Id="AgentTunnelDlgAgentNameHint">Identifier for this agent. Leave blank to use the name in the JWT, or the local computer name as a final fallback.</String>
7072
<String Id="AgentTunnelDlgGatewayUrlLabel">Gateway URL (advanced, optional):</String>
7173
<String Id="AgentTunnelDlgGatewayUrlHint">Override the URL embedded in the enrollment JWT. Leave blank to use the JWT's value.</String>
7274
</WixLocalization>

package/AgentWindowsManaged/Resources/DevolutionsAgent_fr-fr.wxl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<String Id="AgentTunnelDlgSubnetsHint">Notation CIDR séparée par des virgules, p. ex. 10.10.0.0/24, 192.168.1.0/24. Laissez vide pour la détection automatique.</String>
1313
<String Id="AgentTunnelDlgDomainsLabel">Domaines annoncés :</String>
1414
<String Id="AgentTunnelDlgDomainsHint">Suffixes DNS séparés par des virgules que l'agent peut résoudre, p. ex. corp.example.com, lab.example.com. Laissez vide pour ignorer.</String>
15+
<String Id="AgentTunnelDlgAgentNameLabel">Nom de l'agent (facultatif) :</String>
16+
<String Id="AgentTunnelDlgAgentNameHint">Identifiant de cet agent. Laissez vide pour utiliser le nom inscrit dans le JWT, ou le nom de l'ordinateur local comme dernier recours.</String>
1517
<String Id="AgentTunnelDlgGatewayUrlLabel">URL de la passerelle (avancé, facultatif) :</String>
1618
<String Id="AgentTunnelDlgGatewayUrlHint">Remplace l'URL incluse dans le JWT d'enrôlement. Laissez vide pour utiliser la valeur du JWT.</String>
1719
<String Id="Language">1036</String>

0 commit comments

Comments
 (0)