Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
476a16d
add initial admin mode .md file
nagilson Oct 12, 2026
a11f9e8
WIP - initial admin spec; hand-written.
nagilson Oct 12, 2026
d82c305
cleanup and sync with meeting feedback
nagilson Oct 14, 2026
6b8be92
Futher expansion upon dotnetup admin edge cases or potential issues
nagilson Oct 14, 2026
0467f68
block unsigned dailies in admin mode by default
nagilson Oct 14, 2026
815bed7
consider muxer replacement option
nagilson Oct 14, 2026
6b1a39f
reg key should enforce admin mode rather than user local config file
nagilson Oct 14, 2026
2ca2542
fix admin mode to be able to revert it with uac
nagilson Oct 14, 2026
d393b2a
admin mode shuold set block unsigned by default - ugh claude made thi…
nagilson Oct 14, 2026
7070a6b
copilot fb
nagilson Oct 14, 2026
014af5c
add learnings from meeting with @joeloff
nagilson Oct 14, 2026
1cad672
dotnetup: add admin access mode contracts
nagilson Oct 14, 2026
63b4199
dotnetup: tag telemetry with access mode
nagilson Oct 14, 2026
b20d2f2
dotnetup: block unsupported commands in admin mode
nagilson Oct 14, 2026
c41c672
dotnetup: switch admin mode with init --mode and env set
nagilson Oct 14, 2026
f0b5e27
dotnetup: install Windows installer bundles in admin mode
nagilson Oct 14, 2026
2d1ac81
dotnetup: check admin installs without loading hostfxr in process
nagilson Oct 14, 2026
eea1f45
dotnetup: harden admin installer verification and cancellation
nagilson Oct 14, 2026
3aad05c
dotnetup: keep self update and setup working across admin mode changes
nagilson Oct 14, 2026
16ee132
dotnetup: document admin mode MVP
nagilson Oct 14, 2026
fc43e90
env script and legacy print env script shouldn't cause shell failure
nagilson Oct 16, 2026
604edaa
dotnetup: localize admin mode guidance and simplify gate diagnostics
nagilson Oct 16, 2026
dee539d
dotnetup: hide migration choices while admin mode is selected
nagilson Oct 16, 2026
0edd196
dotnetup: preserve the explicit untracked install option
nagilson Oct 16, 2026
4a0d2a6
dotnetup: remove unused legacy framework branches from policy store
nagilson Oct 16, 2026
887dc28
dotnetup: ignore user data overrides for installer bundle caches
nagilson Oct 16, 2026
00fadfc
dotnetup: consolidate admin test fixtures and command coverage
nagilson Oct 16, 2026
86fea34
Dont use user controllable directory for admin installs if dotnetup i…
nagilson Oct 16, 2026
fa84796
dotnetup: annotate the Authenticode verifier with its supported platform
nagilson Oct 16, 2026
3db2c88
dotnetup: report a specific error for non-.exe installer downloads
nagilson Oct 16, 2026
3e78ae2
dotnetup: check only host/fxr version directories for hostfxr
nagilson Oct 16, 2026
1535a03
dotnetup: localize installer orchestration messages
nagilson Oct 16, 2026
7852f07
dotnetup: clarify how env set leaves admin mode
nagilson Oct 16, 2026
a16c9c4
dotnetup: remove admin installer details from the signature verificat…
nagilson Oct 16, 2026
3754e0a
dotnetup: document the architecture assumption in admin root resolution
nagilson Oct 16, 2026
16a4bcf
dotnetup: share host-reported install checks between validators
nagilson Oct 16, 2026
e1effff
dotnetup: keep the admin-mode --untracked check in the install command
nagilson Oct 16, 2026
05fe8bc
fix style gates
nagilson Oct 19, 2026
44a57ec
Merge branch 'release/dnup' into nagilson-dotnetup-admin-mvp
nagilson Oct 19, 2026
1a94289
dotnetup: inject the machine policy instead of static test hooks
nagilson Oct 17, 2026
415fad1
dotnetup: complete policy injection coverage
nagilson Oct 19, 2026
4bc05bf
dotnetup: remove redundant bundle tracking assertion
nagilson Oct 19, 2026
cb30dcc
Fix dynamic dotnetup test assets after net12 transition
nagilson Oct 19, 2026
a35b6ab
Localize Windows installer bundle messages
nagilson Oct 19, 2026
e2fd7cd
Use native hostfxr invocation without parsing --list-runtime but with…
nagilson Oct 19, 2026
89259ef
Fix CI to not require sdk to build dotnetup to get sdks
nagilson Oct 19, 2026
3bd97cd
Simplify output instructions for admin mode
nagilson Oct 19, 2026
9e93fe3
Cancellation message doesnt display if its on stderr
nagilson Oct 19, 2026
6b9329d
Shorten ctrl + c interrupt + allow switching out with init --mode
nagilson Oct 19, 2026
c167eb9
Isolate installer cancellation rendering tests from CI enrichment
nagilson Oct 19, 2026
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
274 changes: 274 additions & 0 deletions documentation/general/dotnetup/designs/dotnetup-admin-mode.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ public enum DotnetInstallErrorCode

/// <summary>The user declined confirmation of an operation.</summary>
OperationCancelled,

/// <summary>The invoked command is not supported while dotnetup is in admin mode.</summary>
AdminModeCommandNotSupported,

/// <summary>An option or argument passed to the command is not supported while dotnetup is in admin mode.</summary>
AdminModeSymbolNotSupported,

/// <summary>The operation requires <c>--untracked</c> while dotnetup is in admin mode.</summary>
AdminModeRequiresUntracked,

/// <summary>Admin mode could not be enabled or disabled because elevation was declined or failed, or the policy could not be written.</summary>
AdminModeChangeFailed,

/// <summary>Another Windows Installer (MSI) installation is in progress (exit code 1618).</summary>
InstallerBusy,

/// <summary>The Windows installer bundle returned a failure exit code.</summary>
InstallerFailed,

/// <summary>The Windows installer bundle reported success, but the expected installation could not be verified afterwards.</summary>
InstallerPostInstallVerificationFailed,
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@ internal record InstallRequestOptions()
/// Corresponds to the <c>--verbosity</c> CLI option.
/// </summary>
public Verbosity Verbosity { get; init; }

/// <summary>
/// How the installation is performed. Defaults to <see cref="InstallMechanism.Archive"/>;
/// admin mode requests use <see cref="InstallMechanism.WindowsInstallerBundle"/>.
/// </summary>
public InstallMechanism InstallMechanism { get; init; } = InstallMechanism.Archive;
}

/// <summary>
/// The mechanism used to lay down a .NET installation.
/// </summary>
internal enum InstallMechanism
{
/// <summary>Download an archive and extract it into the install root.</summary>
Archive = 0,

/// <summary>Run the Windows installer bundle (.exe) into the shared system folder.</summary>
WindowsInstallerBundle = 1,
}

/// <summary>
Expand Down
129 changes: 129 additions & 0 deletions src/Installer/Microsoft.Dotnet.Installation/Internal/DotnetupPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.Versioning;
#if NET
using Microsoft.Win32;
#endif

namespace Microsoft.Dotnet.Installation.Internal;

/// <summary>
/// Reads and writes the machine-wide dotnetup policy flags.
/// </summary>
internal interface IDotnetupPolicyStore
{
/// <summary>
/// Returns true when the named flag is set to a nonzero value. Returns false when the value is
/// missing, unreadable, or not a DWORD.
/// </summary>
bool ReadFlag(string valueName);

/// <summary>
/// Writes each flag as a DWORD 1 (true) or 0 (false), creating the policy key if needed.
/// Throws when the values cannot be written.
/// </summary>
void WriteFlags(IReadOnlyDictionary<string, bool> values);
}

/// <summary>
/// Machine-wide dotnetup policy stored as independent <c>REG_DWORD</c> values under
/// <c>HKLM\SOFTWARE\Policies\Microsoft\dotnet\Dotnetup</c>. Admin mode is Windows-only and is only
/// ever read from this policy store, never from the per-user dotnetup configuration.
/// </summary>
internal static class DotnetupPolicy
{
internal const string WindowsPolicyKey = @"SOFTWARE\Policies\Microsoft\dotnet\Dotnetup";
internal const string AdminModeValueName = "AdminMode";
internal const string BlockUnsignedDownloadsValueName = "BlockUnsignedDownloads";
Comment thread
nagilson marked this conversation as resolved.

/// <summary>
/// The store backing the policy. Defaults to the Windows registry; tests may replace it and must
/// call <see cref="ResetStoreForTesting"/> afterwards.
/// </summary>
internal static IDotnetupPolicyStore Store { get; set; } = CreateDefaultStore();

internal static void ResetStoreForTesting() => Store = CreateDefaultStore();
Comment thread
nagilson marked this conversation as resolved.
Outdated

/// <summary>Returns true when the machine policy enables admin mode. Always false off Windows.</summary>
public static bool IsAdminModeEnabled() =>
OperatingSystem.IsWindows() && Store.ReadFlag(AdminModeValueName);

/// <summary>Returns true when the Windows registry policy blocks unsigned downloads.</summary>
public static bool IsUnsignedDownloadBlockedByRegistry() =>
Store.ReadFlag(BlockUnsignedDownloadsValueName);

/// <summary>Enables admin mode, which also blocks unsigned downloads.</summary>
public static void EnableAdminMode() =>
Store.WriteFlags(new Dictionary<string, bool>
{
[AdminModeValueName] = true,
[BlockUnsignedDownloadsValueName] = true,
});

/// <summary>
/// Disables admin mode. The unsigned download policy is left unchanged because IT may manage it
/// independently.
/// </summary>
public static void DisableAdminMode() =>
Store.WriteFlags(new Dictionary<string, bool>
{
[AdminModeValueName] = false,
});

private static IDotnetupPolicyStore CreateDefaultStore() =>
OperatingSystem.IsWindows() ? new WindowsRegistryPolicyStore() : new UnsupportedPlatformPolicyStore();

[SupportedOSPlatform("windows")]
private sealed class WindowsRegistryPolicyStore : IDotnetupPolicyStore
Comment thread
nagilson marked this conversation as resolved.
Outdated
{
public bool ReadFlag(string valueName)
{
#if NET
try
{
using RegistryKey baseKey = OpenLocalMachine();
using RegistryKey? key = baseKey.OpenSubKey(WindowsPolicyKey);
return key?.GetValue(valueName) is int value && value != 0;
}
catch
{
return false;
}
#else
return false;
#endif
}

public void WriteFlags(IReadOnlyDictionary<string, bool> values)
{
ArgumentNullException.ThrowIfNull(values);
#if NET
Comment thread
nagilson marked this conversation as resolved.
Outdated
using RegistryKey baseKey = OpenLocalMachine();
using RegistryKey key = baseKey.CreateSubKey(WindowsPolicyKey, writable: true)
?? throw new InvalidOperationException($@"Unable to open HKLM\{WindowsPolicyKey} for writing.");
foreach (var (valueName, enabled) in values)
{
key.SetValue(valueName, enabled ? 1 : 0, RegistryValueKind.DWord);
}
#else
throw new PlatformNotSupportedException();
#endif
}

#if NET
private static RegistryKey OpenLocalMachine() =>
RegistryKey.OpenBaseKey(
RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32);
#endif
}

private sealed class UnsupportedPlatformPolicyStore : IDotnetupPolicyStore
{
public bool ReadFlag(string valueName) => false;

public void WriteFlags(IReadOnlyDictionary<string, bool> values) =>
throw new PlatformNotSupportedException("dotnetup machine policy is only supported on Windows.");
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.Versioning;
using Microsoft.Deployment.DotNet.Releases;
#if NET
using Microsoft.Win32;
#endif

namespace Microsoft.Dotnet.Installation.Internal;

Expand All @@ -27,10 +23,10 @@ internal static class UnsignedSourcePolicy
/// <c>HKLM\SOFTWARE\Policies\Microsoft\dotnet\Dotnetup!BlockUnsignedDownloads</c>
/// enforces the policy.
/// </summary>
internal const string WindowsPolicyKey = @"SOFTWARE\Policies\Microsoft\dotnet\Dotnetup";
internal const string WindowsPolicyKey = DotnetupPolicy.WindowsPolicyKey;

/// <summary>The DWORD value name under <see cref="WindowsPolicyKey"/>.</summary>
internal const string WindowsPolicyValueName = "BlockUnsignedDownloads";
internal const string WindowsPolicyValueName = DotnetupPolicy.BlockUnsignedDownloadsValueName;

/// <summary>
/// Sentinel file checked on Linux/macOS. When this file exists the policy is enforced;
Expand Down Expand Up @@ -81,7 +77,7 @@ public static bool IsUnsignedDownloadBlocked()

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return IsWindowsPolicyEnabled();
return DotnetupPolicy.IsUnsignedDownloadBlockedByRegistry();
}

try
Expand All @@ -95,22 +91,4 @@ public static bool IsUnsignedDownloadBlocked()
return false;
}
}

[SupportedOSPlatform("windows")]
private static bool IsWindowsPolicyEnabled()
{
#if NET
try
{
using RegistryKey? key = Registry.LocalMachine.OpenSubKey(WindowsPolicyKey);
return ((int?)key?.GetValue(WindowsPolicyValueName) ?? 0) != 0;
}
catch
{
return false;
}
#else
return false;
#endif
}
}
69 changes: 69 additions & 0 deletions src/Installer/dotnetup.Library/AccessModeResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Dotnet.Installation.Internal;

namespace Microsoft.DotNet.Tools.Bootstrapper;

/// <summary>Where the effective access mode came from.</summary>
internal enum AccessModeSource
{
/// <summary>No access mode is configured.</summary>
Unset,

/// <summary>The access mode recorded in the per-user dotnetup config.</summary>
UserConfig,

/// <summary>The machine policy (admin mode).</summary>
Policy,
}

/// <summary>The access mode dotnetup operates under, and its source.</summary>
internal sealed record EffectiveAccessMode(DotnetAccessMode? Mode, AccessModeSource Source)
{
public bool IsAdmin => Mode == DotnetAccessMode.Admin;

public static EffectiveAccessMode Unset { get; } = new(null, AccessModeSource.Unset);
}

/// <summary>
/// Resolves the effective access mode. The Windows machine policy enables admin mode and takes
/// precedence over the per-user config, which can never select admin mode.
/// </summary>
internal static class AccessModeResolver
{
/// <summary>When non-null, replaces resolution. Tests must reset it to <c>null</c>.</summary>
internal static Func<EffectiveAccessMode>? OverrideForTesting { get; set; }
Comment thread
nagilson marked this conversation as resolved.
Outdated

/// <summary>Resolves the effective access mode. Never throws and never writes output.</summary>
public static EffectiveAccessMode Resolve()
{
var overrideFn = OverrideForTesting;
if (overrideFn is not null)
{
return overrideFn();
}

if (OperatingSystem.IsWindows() && DotnetupPolicy.IsAdminModeEnabled())
{
return new EffectiveAccessMode(DotnetAccessMode.Admin, AccessModeSource.Policy);
}

DotnetAccessMode? configured = TryReadUserConfigAccessMode();
return configured is DotnetAccessMode mode && mode != DotnetAccessMode.Admin
? new EffectiveAccessMode(mode, AccessModeSource.UserConfig)
: EffectiveAccessMode.Unset;
}

private static DotnetAccessMode? TryReadUserConfigAccessMode()
{
try
{
return DotnetupConfig.ReadAccessModeSilently();
}
catch
{
return null;
}
}
}
59 changes: 59 additions & 0 deletions src/Installer/dotnetup.Library/AdminModeSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;

namespace Microsoft.DotNet.Tools.Bootstrapper;

/// <summary>
/// Describes whether a command can run in admin mode and which of its options and arguments are
/// permitted there. Commands are unsupported unless they opt in (default deny).
/// </summary>
internal sealed class AdminModeSupport
{
private AdminModeSupport(bool isSupported, bool allowsAnySymbol, IReadOnlySet<Symbol> allowedSymbols)
{
IsSupported = isSupported;
AllowsAnySymbol = allowsAnySymbol;
AllowedSymbols = allowedSymbols;
}

/// <summary>The command cannot run in admin mode.</summary>
public static AdminModeSupport Unsupported { get; } = new(false, false, CreateSet([]));

/// <summary>The command runs in admin mode with any of its options and arguments.</summary>
public static AdminModeSupport AnySymbol { get; } = new(true, true, CreateSet([]));

/// <summary>The command runs in admin mode, but only with the given options and arguments.</summary>
public static AdminModeSupport Allow(params Symbol[] symbols)
{
ArgumentNullException.ThrowIfNull(symbols);
return new(true, false, CreateSet(symbols));
}

public bool IsSupported { get; }

public bool AllowsAnySymbol { get; }

/// <summary>The permitted options and arguments, compared by reference.</summary>
public IReadOnlySet<Symbol> AllowedSymbols { get; }

/// <summary>Returns true when <paramref name="symbol"/> may be used in admin mode.</summary>
public bool Allows(Symbol symbol)
{
ArgumentNullException.ThrowIfNull(symbol);
return IsSupported && (AllowsAnySymbol || AllowedSymbols.Contains(symbol));
}

private static HashSet<Symbol> CreateSet(Symbol[] symbols)
{
var set = new HashSet<Symbol>(ReferenceEqualityComparer.Instance);
foreach (var symbol in symbols)
{
ArgumentNullException.ThrowIfNull(symbol, nameof(symbols));
set.Add(symbol);
}

return set;
}
}
6 changes: 6 additions & 0 deletions src/Installer/dotnetup.Library/CommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ protected CommandBase(ParseResult parseResult, string commandName)
/// </summary>
protected virtual bool ShowsUpdateNotification => false;

/// <summary>
/// Admin mode support for this command when it is invoked as <paramref name="command"/>.
/// Default: unsupported (default deny).
/// </summary>
internal virtual AdminModeSupport GetAdminModeSupport(Command command) => AdminModeSupport.Unsupported;

public int Execute()
{
_exitCode = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public ElevatedSystemPathCommand(ParseResult result) : base(result, "elevatedsys
_dotnetDir = result.GetValue(ElevatedSystemPathCommandParser.DotnetDir);
}

/// <inheritdoc/>
internal override AdminModeSupport GetAdminModeSupport(Command command) => AdminModeSupport.AnySymbol;

private void Log(string message)
{
Console.WriteLine(message);
Expand Down
Loading