Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions DialogManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -317,12 +317,43 @@ private void LaunchDialog(List<string> argList)
CreateNoWindow = false,
};

_dialogProcess = Process.Start(startInfo);
// A shell-executed child cannot be given its own environment, only inherit
// ours, so the key is set on this process for the launch and removed again.
// It never goes on the command line or into the log.
var authKey = BootstrapMate.Core.DialogAuthKey.Resolve(
BootstrapMate.Core.ConfigManager.Instance.Config.DialogAuthKeyPath);
var callerKey = Environment.GetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable);
try
{
if (authKey != null)
Environment.SetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable, authKey);
_dialogProcess = Process.Start(startInfo);
}
finally
{
Environment.SetEnvironmentVariable(BootstrapMate.Core.DialogAuthKey.EnvironmentVariable, callerKey);
}

// A dialog that refuses the key exits at once with 30. Catch that here so the
// run carries on headless instead of writing to a dialog that is not there.
if (_dialogProcess != null && _dialogProcess.WaitForExit(3000) &&
_dialogProcess.ExitCode == BootstrapMate.Core.DialogAuthKey.KeyRequiredExitCode)
{
Logger.Warning(authKey == null
? "csharpdialog requires an authorisation key and none was found; continuing without the dialog"
: "csharpdialog rejected the authorisation key; continuing without the dialog");
_dialogProcess.Dispose();
_dialogProcess = null;
_isAvailable = false;
return;
}

if (_dialogProcess != null)
{
_isRunning = true;
Logger.Info("csharpdialog launched successfully");
Logger.Info(authKey != null
? "csharpdialog launched successfully (authorisation key supplied)"
: "csharpdialog launched successfully");
}
else
{
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ no simulated install, so a policy-set `DryRun` exits `1` without installing anyt
sets the dialog's icon. `SilentMode` and `VerboseMode` from policy or settings turn
those modes on, and a CLI switch cannot turn them off.

## csharpDialog authorisation key

When csharpDialog is managed with an authorisation key, it opens only for a caller that
passes the matching plain key in the `DIALOG_AUTH_KEY` environment variable, and otherwise
exits `30`. BootstrapMate passes its own `DIALOG_AUTH_KEY` through when it has one, and
otherwise reads the key from the file named by `DialogAuthKeyPath` (policy or machine
settings; default `C:\ProgramData\ManagedNotifications\authkey`), which should be readable
only by SYSTEM and Administrators. The key is never put on the command line or logged. With
no key, or a wrong one, the dialog does not open, a warning is logged, and the run carries
on without it.

## Registry Status Contract

BootstrapMate tracks completion status in both 64-bit and 32-bit registry views:
Expand Down
13 changes: 13 additions & 0 deletions resources/BootstrapMate.admx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@
</elements>
</policy>

<policy name="DialogAuthKeyPath"
class="Machine"
displayName="$(string.DialogAuthKeyPath)"
explainText="$(string.DialogAuthKeyPath_Help)"
key="SOFTWARE\Policies\BootstrapMate"
presentation="$(presentation.DialogAuthKeyPath)">
<parentCategory ref="Dialog" />
<supportedOn ref="windows:SUPPORTED_Windows_10_0" />
<elements>
<text id="DialogAuthKeyPath_Value" valueName="DialogAuthKeyPath" />
</elements>
</policy>

<policy name="BlurScreen"
class="Machine"
displayName="$(string.BlurScreen)"
Expand Down
13 changes: 13 additions & 0 deletions resources/en-US/BootstrapMate.adml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ Default: Please wait while we install required software...</string>

Leave empty to use the default icon.</string>

<string id="DialogAuthKeyPath">Dialog Authorisation Key File</string>
<string id="DialogAuthKeyPath_Help">Path to a file holding the plain csharpDialog authorisation key. When csharpDialog is managed with an authorisation key, BootstrapMate reads this file and passes the key to the dialog in the DIALOG_AUTH_KEY environment variable. The file should be readable only by SYSTEM and Administrators.

If the key is missing or wrong the dialog does not open and the run continues without it.

Default: C:\ProgramData\ManagedNotifications\authkey</string>

<string id="BlurScreen">Blur Screen Behind Dialog</string>
<string id="BlurScreen_Help">When enabled, the screen behind the progress dialog is blurred to prevent user interaction during setup.

Expand Down Expand Up @@ -162,6 +169,12 @@ Default: Disabled</string>
</textBox>
</presentation>

<presentation id="DialogAuthKeyPath">
<textBox refId="DialogAuthKeyPath_Value">
<label>Key File Path:</label>
</textBox>
</presentation>

<presentation id="CustomInstallPath">
<textBox refId="CustomInstallPath_Value">
<label>Install Path:</label>
Expand Down
2 changes: 2 additions & 0 deletions src/BootstrapMate.Core/BootstrapMateConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public sealed class BootstrapMateConfig
public string? DialogIcon { get; set; }
public bool BlurScreen { get; set; }
public bool NoDialog { get; set; }
/// <summary>File holding the plain csharpDialog authorisation key; null means the default path.</summary>
public string? DialogAuthKeyPath { get; set; }

// Reporting: vendor-neutral run-summary POST
public string? ReportingUrl { get; set; }
Expand Down
6 changes: 6 additions & 0 deletions src/BootstrapMate.Core/BootstrapMateConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public static class BootstrapMateConstants
/// <summary>Default dialog message.</summary>
public const string DefaultDialogMessage = "Please wait while we install required software...";

/// <summary>
/// Where the csharpDialog authorisation key is read from when no DialogAuthKeyPath is set.
/// The file should be readable only by SYSTEM and Administrators.
/// </summary>
public const string DefaultDialogAuthKeyPath = @"C:\ProgramData\ManagedNotifications\authkey";

/// <summary>Named pipe prefix for GUI ↔ CLI communication.</summary>
public const string PipeNamePrefix = "BootstrapMate_";

Expand Down
5 changes: 5 additions & 0 deletions src/BootstrapMate.Core/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ void WriteInt(string key, int value)
WriteString("DialogMessage", settings.DialogMessage);
WriteString("DialogIcon", settings.DialogIcon);
WriteBool("BlurScreen", settings.BlurScreen);
WriteString("DialogAuthKeyPath", settings.DialogAuthKeyPath);
WriteString("CustomInstallPath", settings.CustomInstallPath);
WriteInt("NetworkTimeout", settings.NetworkTimeout);
WriteInt("BaselineMinIntervalHours", settings.BaselineMinIntervalHours);
Expand Down Expand Up @@ -265,6 +266,9 @@ private void LoadFromManagement(ManagementDetector management)
if (management.GetManagedString("DialogIcon") is { Length: > 0 } icon)
Config.DialogIcon = icon;

if (management.GetManagedString("DialogAuthKeyPath") is { Length: > 0 } keyPath)
Config.DialogAuthKeyPath = keyPath;

if (management.GetManagedBool("BlurScreen") is { } blur)
Config.BlurScreen = blur;

Expand Down Expand Up @@ -330,6 +334,7 @@ private void LoadFromHive(RegistryHive hive, ConfigSource source, RegistryView v
Config.DialogMessage = ReadString(settingsKey, "DialogMessage") ?? Config.DialogMessage;
Config.DialogIcon = ReadString(settingsKey, "DialogIcon") ?? Config.DialogIcon;
Config.BlurScreen = ReadBool(settingsKey, "BlurScreen") ?? Config.BlurScreen;
Config.DialogAuthKeyPath = ReadString(settingsKey, "DialogAuthKeyPath") ?? Config.DialogAuthKeyPath;
Config.CustomInstallPath = ReadString(settingsKey, "CustomInstallPath") ?? Config.CustomInstallPath;
Config.NetworkTimeout = ReadInt(settingsKey, "NetworkTimeout") ?? Config.NetworkTimeout;
Config.BaselineMinIntervalHours = ReadInt(settingsKey, "BaselineMinIntervalHours") ?? Config.BaselineMinIntervalHours;
Expand Down
41 changes: 41 additions & 0 deletions src/BootstrapMate.Core/DialogAuthKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace BootstrapMate.Core;

/// <summary>
/// The csharpDialog authorisation key. When policy sets an AuthorisationKey for csharpDialog,
/// the dialog refuses to open (exit 30) unless its caller passes the matching plain key in the
/// DIALOG_AUTH_KEY environment variable. The key reaches this process either already in that
/// variable or in a file only SYSTEM and Administrators can read.
/// </summary>
public static class DialogAuthKey
{
/// <summary>The environment variable csharpDialog reads the key from.</summary>
public const string EnvironmentVariable = "DIALOG_AUTH_KEY";

/// <summary>The exit code csharpDialog returns when the key is missing or wrong.</summary>
public const int KeyRequiredExitCode = 30;

/// <summary>
/// The key to hand csharpDialog, or null to launch it without one: the caller's own
/// DIALOG_AUTH_KEY if set, otherwise the trimmed contents of <paramref name="path"/>
/// (the default path when null or empty). A missing or unreadable file is not an error.
/// </summary>
public static string? Resolve(string? path, Func<string, string?>? readEnvironment = null)
{
var fromEnvironment = (readEnvironment ?? Environment.GetEnvironmentVariable)(EnvironmentVariable);
if (!string.IsNullOrWhiteSpace(fromEnvironment))
return fromEnvironment.Trim();

var file = string.IsNullOrWhiteSpace(path) ? BootstrapMateConstants.DefaultDialogAuthKeyPath : path;
try
{
if (!File.Exists(file))
return null;
var key = File.ReadAllText(file).Trim();
return key.Length > 0 ? key : null;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return null;
}
}
}
1 change: 1 addition & 0 deletions src/BootstrapMate.Core/ManagementDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public sealed class ManagementDetector
["DialogMessage"] = ["DialogMessage"],
["DialogIcon"] = ["DialogIcon"],
["BlurScreen"] = ["BlurScreen"],
["DialogAuthKeyPath"] = ["DialogAuthKeyPath"],
["CustomInstallPath"] = ["CustomInstallPath", "InstallPath", "iapath"],
["NetworkTimeout"] = ["NetworkTimeout"],
["BaselineMinIntervalHours"] = ["BaselineMinIntervalHours"],
Expand Down
43 changes: 43 additions & 0 deletions tests/BootstrapMate.Core.Tests/DialogAuthKeyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using BootstrapMate.Core;
using Xunit;

namespace BootstrapMate.Core.Tests;

public class DialogAuthKeyTests : IDisposable
{
private readonly string _dir = Path.Combine(Path.GetTempPath(), "bm-authkey-" + Guid.NewGuid().ToString("N"));

public DialogAuthKeyTests() => Directory.CreateDirectory(_dir);

public void Dispose() => Directory.Delete(_dir, recursive: true);

private static string? NoEnvironment(string _) => null;

[Fact]
public void CallerEnvironmentWins()
{
var file = Path.Combine(_dir, "authkey");
File.WriteAllText(file, "from-file");
Assert.Equal("from-env", DialogAuthKey.Resolve(file, _ => " from-env "));
}

[Fact]
public void FileIsReadAndTrimmed()
{
var file = Path.Combine(_dir, "authkey");
File.WriteAllText(file, " secret-key\r\n");
Assert.Equal("secret-key", DialogAuthKey.Resolve(file, NoEnvironment));
}

[Fact]
public void MissingFileMeansNoKey() =>
Assert.Null(DialogAuthKey.Resolve(Path.Combine(_dir, "absent"), NoEnvironment));

[Fact]
public void EmptyFileMeansNoKey()
{
var file = Path.Combine(_dir, "authkey");
File.WriteAllText(file, "\r\n");
Assert.Null(DialogAuthKey.Resolve(file, NoEnvironment));
}
}
Loading