Skip to content

Commit 88868b7

Browse files
hangyadamsitnik
andauthored
Extend ProcessStartInfo to allow setting LOGON_NETCREDENTIALS_ONLY (#77637)
Co-authored-by: Adam Sitnik <[email protected]>
1 parent f3e2260 commit 88868b7

File tree

13 files changed

+381
-135
lines changed

13 files changed

+381
-135
lines changed

src/libraries/Common/tests/TestUtilities/System/WindowsIdentityFixture.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public sealed partial class WindowsTestAccount : IDisposable
3434
private readonly string _userName;
3535
private SafeAccessTokenHandle _accountTokenHandle;
3636
public SafeAccessTokenHandle AccountTokenHandle => _accountTokenHandle;
37-
public string AccountName { get; set; }
37+
public string AccountName { get; private set; }
3838
public string Password { get; }
3939

4040
public WindowsTestAccount(string userName)
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Runtime.InteropServices;
5+
using System.ServiceProcess;
6+
7+
namespace System
8+
{
9+
public sealed partial class WindowsTestFileShare : IDisposable
10+
{
11+
private static readonly Lazy<bool> _canShareFiles = new Lazy<bool>(() =>
12+
{
13+
if (!PlatformDetection.IsWindows || !PlatformDetection.IsPrivilegedProcess)
14+
{
15+
return false;
16+
}
17+
18+
try
19+
{
20+
// the "Server Service" allows for file sharing. It can be disabled on some machines.
21+
using (ServiceController sharingService = new ServiceController("Server"))
22+
{
23+
return sharingService.Status == ServiceControllerStatus.Running;
24+
}
25+
}
26+
catch (InvalidOperationException)
27+
{
28+
// The service is not installed.
29+
return false;
30+
}
31+
});
32+
33+
private readonly string _shareName;
34+
35+
private readonly string _path;
36+
37+
private bool _disposedValue;
38+
39+
public WindowsTestFileShare(string shareName, string path)
40+
{
41+
_shareName = shareName;
42+
_path = path;
43+
Initialize();
44+
}
45+
46+
public static bool CanShareFiles => _canShareFiles.Value;
47+
48+
private void Initialize()
49+
{
50+
SHARE_INFO_502 shareInfo = default;
51+
shareInfo.shi502_netname = _shareName;
52+
shareInfo.shi502_path = _path;
53+
shareInfo.shi502_remark = "folder created to test UNC file paths";
54+
shareInfo.shi502_max_uses = -1;
55+
56+
int infoSize = Marshal.SizeOf(shareInfo);
57+
IntPtr infoBuffer = Marshal.AllocCoTaskMem(infoSize);
58+
59+
try
60+
{
61+
Marshal.StructureToPtr(shareInfo, infoBuffer, false);
62+
63+
const int NERR_DuplicateShare = 2118;
64+
int shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero);
65+
if (shareResult == NERR_DuplicateShare)
66+
{
67+
NetShareDel(string.Empty, _shareName, 0);
68+
shareResult = NetShareAdd(string.Empty, 502, infoBuffer, IntPtr.Zero);
69+
}
70+
71+
if (shareResult != 0 && shareResult != NERR_DuplicateShare)
72+
{
73+
throw new Exception($"Failed to create a file share, NetShareAdd returned {shareResult}");
74+
}
75+
}
76+
finally
77+
{
78+
Marshal.FreeCoTaskMem(infoBuffer);
79+
}
80+
}
81+
82+
[LibraryImport(Interop.Libraries.Netapi32)]
83+
private static partial int NetShareAdd([MarshalAs(UnmanagedType.LPWStr)] string servername, int level, IntPtr buf, IntPtr parm_err);
84+
85+
[LibraryImport(Interop.Libraries.Netapi32)]
86+
private static partial int NetShareDel([MarshalAs(UnmanagedType.LPWStr)] string servername, [MarshalAs(UnmanagedType.LPWStr)] string netname, int reserved);
87+
88+
public void Dispose()
89+
{
90+
if (_disposedValue)
91+
{
92+
return;
93+
}
94+
95+
NetShareDel(string.Empty, _shareName, 0);
96+
_disposedValue = true;
97+
}
98+
99+
[StructLayout(LayoutKind.Sequential)]
100+
internal struct SHARE_INFO_502
101+
{
102+
[MarshalAs(UnmanagedType.LPWStr)]
103+
public string shi502_netname;
104+
public uint shi502_type;
105+
[MarshalAs(UnmanagedType.LPWStr)]
106+
public string shi502_remark;
107+
public int shi502_permissions;
108+
public int shi502_max_uses;
109+
public int shi502_current_uses;
110+
[MarshalAs(UnmanagedType.LPWStr)]
111+
public string shi502_path;
112+
public IntPtr shi502_passwd;
113+
public int shi502_reserved;
114+
public IntPtr shi502_security_descriptor;
115+
}
116+
}
117+
}
118+

src/libraries/System.Diagnostics.Process/ref/System.Diagnostics.Process.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,8 @@ public ProcessStartInfo(string fileName, string arguments) { }
232232
public string FileName { get { throw null; } set { } }
233233
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
234234
public bool LoadUserProfile { get { throw null; } set { } }
235+
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
236+
public bool UseCredentialsForNetworkingOnly { get { throw null; } set { } }
235237
[System.CLSCompliantAttribute(false)]
236238
[System.Runtime.Versioning.SupportedOSPlatformAttribute("windows")]
237239
public System.Security.SecureString? Password { get { throw null; } set { } }

src/libraries/System.Diagnostics.Process/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@
269269
<data name="CantSetDuplicatePassword" xml:space="preserve">
270270
<value>ProcessStartInfo.Password and ProcessStartInfo.PasswordInClearText cannot both be set. Use only one of them.</value>
271271
</data>
272+
<data name="CantEnableConflictingLogonFlags" xml:space="preserve">
273+
<value>ProcessStartInfo.LoadUserProfile and ProcessStartInfo.UseCredentialsForNetworkingOnly cannot both be set. Use only one of them.</value>
274+
</data>
272275
<data name="ArgumentOutOfRange_IndexCountBuffer" xml:space="preserve">
273276
<value>Index and count must refer to a location within the buffer.</value>
274277
</data>

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,18 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
524524
}
525525

526526
Interop.Advapi32.LogonFlags logonFlags = (Interop.Advapi32.LogonFlags)0;
527-
if (startInfo.LoadUserProfile)
527+
if (startInfo.LoadUserProfile && startInfo.UseCredentialsForNetworkingOnly)
528+
{
529+
throw new ArgumentException(SR.CantEnableConflictingLogonFlags, nameof(startInfo));
530+
}
531+
else if (startInfo.LoadUserProfile)
528532
{
529533
logonFlags = Interop.Advapi32.LogonFlags.LOGON_WITH_PROFILE;
530534
}
535+
else if (startInfo.UseCredentialsForNetworkingOnly)
536+
{
537+
logonFlags = Interop.Advapi32.LogonFlags.LOGON_NETCREDENTIALS_ONLY;
538+
}
531539

532540
fixed (char* passwordInClearTextPtr = startInfo.PasswordInClearText ?? string.Empty)
533541
fixed (char* environmentBlockPtr = environmentBlock)

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Unix.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ public bool LoadUserProfile
3434
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(LoadUserProfile))); }
3535
}
3636

37+
[SupportedOSPlatform("windows")]
38+
public bool UseCredentialsForNetworkingOnly
39+
{
40+
get { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
41+
set { throw new PlatformNotSupportedException(SR.Format(SR.ProcessStartSingleFeatureNotSupported, nameof(UseCredentialsForNetworkingOnly))); }
42+
}
43+
3744
public bool UseShellExecute { get; set; }
3845

3946
public string[] Verbs => Array.Empty<string>();

src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.Windows.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ public string Domain
2525
[SupportedOSPlatform("windows")]
2626
public bool LoadUserProfile { get; set; }
2727

28+
/// <summary>
29+
/// Gets or sets a value that indicates whether the user credentials
30+
/// are only to be used for network resources.
31+
/// </summary>
32+
/// <value><c>true</c> if the user credentials are only to be used for
33+
/// network resources.</value>
34+
/// <remarks>
35+
/// <para>This property is referenced if the process is being started
36+
/// by using the user name, password, and domain.</para>
37+
/// <para>If the value is <c>true</c>, the process is started with the
38+
/// caller's identity. The system creates a new logon session with
39+
/// the given credentials, which is used on the network only.</para>
40+
/// <para>The system does not validate the specified credentials. Therefore,
41+
/// the process can start, but it may not have access to network resources.</para>
42+
/// </remarks>
43+
[SupportedOSPlatform("windows")]
44+
public bool UseCredentialsForNetworkingOnly { get; set; }
45+
2846
[CLSCompliant(false)]
2947
[SupportedOSPlatform("windows")]
3048
public SecureString? Password { get; set; }
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.IO;
5+
using System.Security;
6+
using Microsoft.DotNet.RemoteExecutor;
7+
using Xunit;
8+
9+
namespace System.Diagnostics.Tests
10+
{
11+
partial class ProcessStartInfoTests : ProcessTestBase
12+
{
13+
private static bool IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles
14+
=> IsAdmin_IsNotNano_RemoteExecutorIsSupported && WindowsTestFileShare.CanShareFiles;
15+
16+
[ConditionalFact(nameof(IsAdmin_IsNotNano_RemoteExecutorIsSupported_CanShareFiles))] // Nano has no "netapi32.dll", Admin rights are required
17+
[PlatformSpecific(TestPlatforms.Windows)]
18+
[OuterLoop("Requires admin privileges")]
19+
public void TestUserNetworkCredentialsPropertiesOnWindows()
20+
{
21+
const string ShareName = "testForDotNet";
22+
const string TestFileContent = "42";
23+
const string UncPathEnvVar = nameof(UncPathEnvVar);
24+
25+
string testFilePath = GetTestFilePath();
26+
File.WriteAllText(testFilePath, TestFileContent);
27+
28+
using WindowsTestFileShare fileShare = new WindowsTestFileShare(ShareName, Path.GetDirectoryName(testFilePath));
29+
string testFileUncPath = $"\\\\{Environment.MachineName}\\{ShareName}\\{Path.GetFileName(testFilePath)}";
30+
31+
using Process process = CreateProcess(() =>
32+
{
33+
try
34+
{
35+
Assert.Equal(TestFileContent, File.ReadAllText(Environment.GetEnvironmentVariable(UncPathEnvVar)));
36+
37+
return RemoteExecutor.SuccessExitCode;
38+
}
39+
catch (Exception ex) when (ex is SecurityException or UnauthorizedAccessException)
40+
{
41+
return -1;
42+
}
43+
});
44+
process.StartInfo.Environment[UncPathEnvVar] = testFileUncPath;
45+
process.StartInfo.UseCredentialsForNetworkingOnly = true;
46+
47+
using TestProcessState processInfo = CreateUserAndExecute(process, Setup, Cleanup);
48+
49+
Assert.Equal(Environment.UserName, Helpers.GetProcessUserName(process));
50+
51+
Assert.True(process.WaitForExit(WaitInMS));
52+
Assert.Equal(RemoteExecutor.SuccessExitCode, process.ExitCode);
53+
54+
void Setup(string username, string _)
55+
{
56+
if (PlatformDetection.IsNotWindowsServerCore) // for this particular Windows version it fails with Attempted to perform an unauthorized operation (#46619)
57+
{
58+
SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: true);
59+
}
60+
}
61+
62+
void Cleanup(string username, string _)
63+
{
64+
if (PlatformDetection.IsNotWindowsServerCore)
65+
{
66+
// remove the access
67+
SetAccessControl(username, testFilePath, Path.GetDirectoryName(testFilePath), add: false);
68+
}
69+
}
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)