Skip to content

Commit

Permalink
Extend ProcessStartInfo to allow setting LOGON_NETCREDENTIALS_ONLY (d…
Browse files Browse the repository at this point in the history
…otnet#77637)

Co-authored-by: Adam Sitnik <[email protected]>
  • Loading branch information
hangy and adamsitnik authored Dec 23, 2022
1 parent f3e2260 commit 88868b7
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public sealed partial class WindowsTestAccount : IDisposable
private readonly string _userName;
private SafeAccessTokenHandle _accountTokenHandle;
public SafeAccessTokenHandle AccountTokenHandle => _accountTokenHandle;
public string AccountName { get; set; }
public string AccountName { get; private set; }
public string Password { get; }

public WindowsTestAccount(string userName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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.InteropServices;
using System.ServiceProcess;

namespace System
{
public sealed partial class WindowsTestFileShare : IDisposable
{
private static readonly Lazy<bool> _canShareFiles = new Lazy<bool>(() =>
{
if (!PlatformDetection.IsWindows || !PlatformDetection.IsPrivilegedProcess)
{
return false;
}
try
{
// the "Server Service" allows for file sharing. It can be disabled on some machines.
using (ServiceController sharingService = new ServiceController("Server"))
{
return sharingService.Status == ServiceControllerStatus.Running;
}
}
catch (InvalidOperationException)
{
// The service is not installed.
return false;
}
});

private readonly string _shareName;

private readonly string _path;

private bool _disposedValue;

public WindowsTestFileShare(string shareName, string path)
{
_shareName = shareName;
_path = path;
Initialize();
}

public static bool CanShareFiles => _canShareFiles.Value;

private void Initialize()
{
SHARE_INFO_502 shareInfo = default;
shareInfo.shi502_netname = _shareName;
shareInfo.shi502_path = _path;
shareInfo.shi502_remark = "folder created to test UNC file paths";
shareInfo.shi502_max_uses = -1;

int infoSize = Marshal.SizeOf(shareInfo);
IntPtr infoBuffer = Marshal.AllocCoTaskMem(infoSize);

try
{
Marshal.StructureToPtr(shareInfo, infoBuffer, false);

const int NERR_DuplicateShare = 2118;
int shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero);
if (shareResult == NERR_DuplicateShare)
{
NetShareDel(string.Empty, _shareName, 0);
shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero);
}

if (shareResult != 0 && shareResult != NERR_DuplicateShare)
{
throw new Exception($"Failed to create a file share, NetShareAdd returned {shareResult}");
}
}
finally
{
Marshal.FreeCoTaskMem(infoBuffer);
}
}

[LibraryImport(Interop.Libraries.Netapi32)]
private static partial int NetShareAdd([MarshalAs(UnmanagedType.LPWStr)] string servername, int level, IntPtr buf, IntPtr parm_err);

[LibraryImport(Interop.Libraries.Netapi32)]
private static partial int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved);

public void Dispose()
{
if (_disposedValue)
{
return;
}

NetShareDel(string.Empty, _shareName, 0);
_disposedValue = true;
}

[StructLayout(LayoutKind.Sequential)]
internal struct SHARE_INFO_502
{
[MarshalAs(UnmanagedType.LPWStr)]
public string shi502_netname;
public uint shi502_type;
[MarshalAs(UnmanagedType.LPWStr)]
public string shi502_remark;
public int shi502_permissions;
public int shi502_max_uses;
public int shi502_current_uses;
[MarshalAs(UnmanagedType.LPWStr)]
public string shi502_path;
public IntPtr shi502_passwd;
public int shi502_reserved;
public IntPtr shi502_security_descriptor;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ public ProcessStartInfo(string fileName, string arguments) { }
public string FileName { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool LoadUserProfile { get { throw null; } set { } }
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } }
[System.CLSCompliantAttribute(false)]
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
public System.Security.SecureString? Password { get { throw null; } set { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@
<data name="CantSetDuplicatePassword" xml:space="preserve">
<value>ProcessStartInfo.Password and ProcessStartInfo.PasswordInClearText cannot both be set. Use only one of them.</value>
</data>
<data name="CantEnableConflictingLogonFlags" xml:space="preserve">
<value>ProcessStartInfo.LoadUserProfile and ProcessStartInfo.UseCredentialsForNetworkingOnly cannot both be set. Use only one of them.</value>
</data>
<data name="ArgumentOutOfRange_IndexCountBuffer" xml:space="preserve">
<value>Index and count must refer to a location within the buffer.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,10 +524,18 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
}

Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0;
if (startInfo.LoadUserProfile)
if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly)
{
throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo));
}
else if (startInfo.LoadUserProfile)
{
logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE;
}
else if (startInfo.UseCredentialsForNetworkingOnly)
{
logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY;
}

fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty)
fixed (char* environmentBlockPtr = environmentBlock)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ public bool LoadUserProfile
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(LoadUserProfile))); }
}

[SupportedOSPlatform("windows")]
public bool UseCredentialsForNetworkingOnly
{
get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
}

public bool UseShellExecute { get; set; }

public string[] Verbs => Array.Empty<string>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ public string Domain
[SupportedOSPlatform("windows")]
public bool LoadUserProfile { get; set; }

/// <summary>
/// Gets or sets a value that indicates whether the user credentials
/// are only to be used for network resources.
/// </summary>
/// <value><c>true</c> if the user credentials are only to be used for
/// network resources.</value>
/// <remarks>
/// <para>This property is referenced if the process is being started
/// by using the user name, password, and domain.</para>
/// <para>If the value is <c>true</c>, the process is started with the
/// caller's identity. The system creates a new logon session with
/// the given credentials, which is used on the network only.</para>
/// <para>The system does not validate the specified credentials. Therefore,
/// the process can start, but it may not have access to network resources.</para>
/// </remarks>
[SupportedOSPlatform("windows")]
public bool UseCredentialsForNetworkingOnly { get; set; }

[CLSCompliant(false)]
[SupportedOSPlatform("windows")]
public SecureString? Password { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Security;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace System.Diagnostics.Tests
{
partial class ProcessStartInfoTests : ProcessTestBase
{
private static bool IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles
=> IsAdmin_IsNotNano_RemoteExecutorIsSupported && WindowsTestFileShare.CanShareFiles;

[ConditionalFact(nameof(IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles))] // Nano has no "netapi32.dll", Admin rights are required
[PlatformSpecific(TestPlatforms.Windows)]
[OuterLoop("Requires admin privileges")]
public void TestUserNetworkCredentialsPropertiesOnWindows()
{
const string ShareName = "testForDotNet";
const string TestFileContent = "42";
const string UncPathEnvVar = nameof(UncPathEnvVar);

string testFilePath = GetTestFilePath();
File.WriteAllText(testFilePath, TestFileContent);

using WindowsTestFileShare fileShare = new WindowsTestFileShare(ShareName, Path.GetDirectoryName(testFilePath));
string testFileUncPath = $"\\\\{Environment.MachineName}\\{ShareName}\\{Path.GetFileName(testFilePath)}";

using Process process = CreateProcess(() =>
{
try
{
Assert.Equal(TestFileContent, File.ReadAllText(Environment.GetEnvironmentVariable(UncPathEnvVar)));
return RemoteExecutor.SuccessExitCode;
}
catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException)
{
return -1;
}
});
process.StartInfo.Environment[UncPathEnvVar] = testFileUncPath;
process.StartInfo.UseCredentialsForNetworkingOnly = true;

using TestProcessState processInfo = CreateUserAndExecute(process, Setup, Cleanup);

Assert.Equal(Environment.UserName, Helpers.GetProcessUserName(process));

Assert.True(process.WaitForExit(WaitInMS));
Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode);

void Setup(string username, string _)
{
if (PlatformDetection.IsNotWindowsServerCore) // for this particular Windows version it fails with Attempted to perform an unauthorized operation (#46619)
{
SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: true);
}
}

void Cleanup(string username, string _)
{
if (PlatformDetection.IsNotWindowsServerCore)
{
// remove the access
SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: false);
}
}
}
}
}
Loading

0 comments on commit 88868b7

Please sign in to comment.