diff --git a/contrib/win32/openssh/OpenSSHTestHelper.psm1 b/contrib/win32/openssh/OpenSSHTestHelper.psm1 index 84f3ce07f6d3..2fc90313bd3c 100644 --- a/contrib/win32/openssh/OpenSSHTestHelper.psm1 +++ b/contrib/win32/openssh/OpenSSHTestHelper.psm1 @@ -15,8 +15,9 @@ $PubKeyUser = "sshtest_pubkeyuser" $PasswdUser = "sshtest_passwduser" $AdminUser = "sshtest_adminuser" $NonAdminUser = "sshtest_nonadminuser" +$SshdUser = "sshd_user" $OpenSSHTestAccountsPassword = "Bulldog_123456" -$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser +$OpenSSHTestAccounts = $Script:SSOUser, $Script:PubKeyUser, $Script:PasswdUser, $Script:AdminUser, $Script:NonAdminUser, $Script:SshdUser $SSHDTestSvcName = "sshdTestSvc" $Script:TestDataPath = "$env:SystemDrive\OpenSSHTests" @@ -69,6 +70,7 @@ function Set-OpenSSHTestEnvironment $Global:OpenSSHTestInfo.Add("PasswdUser", $PasswdUser) # test user to be used for password auth $Global:OpenSSHTestInfo.Add("AdminUser", $AdminUser) # test user to be used for admin logging tests $Global:OpenSSHTestInfo.Add("NonAdminUser", $NonAdminUser) # test user to be used for non-admin logging tests + $Global:OpenSSHTestInfo.Add("SshdUser", $SshdUser) # non-admin local user used by UserEnvironment tests $Global:OpenSSHTestInfo.Add("TestAccountPW", $OpenSSHTestAccountsPassword) # common password for all test accounts $Global:OpenSSHTestInfo.Add("DebugMode", $DebugMode.IsPresent) # run openssh E2E in debug mode $Global:OpenSSHTestInfo.Add("DelayTime", 3) # delay between stoppig sshd service and trying to access log files @@ -225,6 +227,9 @@ WARNING: Following changes will be made to OpenSSH configuration $NonAdminUserProfile = Get-LocalUserProfile -User $NonAdminUser $Global:OpenSSHTestInfo.Add("NonAdminUserProfile", $NonAdminUserProfile) + $SshdUserProfile = Get-LocalUserProfile -User $SshdUser + $Global:OpenSSHTestInfo.Add("SshdUserProfile", $SshdUserProfile) + #make $AdminUser admin net localgroup Administrators $AdminUser /add diff --git a/contrib/win32/win32compat/w32fd.c b/contrib/win32/win32compat/w32fd.c index b4c436864dc4..358f14c04971 100644 --- a/contrib/win32/win32compat/w32fd.c +++ b/contrib/win32/win32compat/w32fd.c @@ -1055,43 +1055,37 @@ int fork() } char * build_commandline_string(const char* cmd, char *const argv[], BOOLEAN prepend_module_path); -wchar_t* -get_username_from_token(HANDLE as_user) +static BOOL +is_sshd_service_token(HANDLE token) { - wchar_t* user_name = NULL; - SID_NAME_USE usage; + BOOL is_sshd = FALSE; DWORD count = 0; - GetTokenInformation(as_user, TokenUser, NULL, 0, &count); - if (count) { - void* buffer = malloc(count); - if (buffer) { - if (GetTokenInformation(as_user, TokenUser, buffer, count, &count)) { - TOKEN_USER* owner = (TOKEN_USER*)buffer; - DWORD name_length = 0; - DWORD domain_length = 0; - LookupAccountSidW(NULL, owner->User.Sid, NULL, &name_length, NULL, &domain_length, &usage); /* Figure out the length of the name. */ - if (name_length) { - wchar_t* domain_name = malloc(domain_length * sizeof(wchar_t)); - if (domain_name) { - user_name = malloc(name_length * sizeof(wchar_t)); - if (user_name) { - memset(user_name, 0, name_length * sizeof(wchar_t)); - memset(domain_name, 0, domain_length * sizeof(wchar_t)); - BOOL success = LookupAccountSidW(NULL, owner->User.Sid, user_name, &name_length, domain_name, &domain_length, &usage); - if (!success) /* Silently return an empty string if unsuccessful. */ - { - free(user_name); - user_name = NULL; - } - } - free(domain_name); - } - } + if (GetTokenInformation(token, TokenUser, NULL, 0, &count) || + GetLastError() != ERROR_INSUFFICIENT_BUFFER || !count) + return FALSE; + void* buffer = malloc(count); + if (!buffer) + return FALSE; + if (GetTokenInformation(token, TokenUser, buffer, count, &count)) { + TOKEN_USER* user = (TOKEN_USER*)buffer; + SID_NAME_USE usage; + DWORD name_len = 0, domain_len = 0; + LookupAccountSidW(NULL, user->User.Sid, NULL, &name_len, NULL, &domain_len, &usage); + if (name_len && domain_len) { + wchar_t* name = malloc(name_len * sizeof(wchar_t)); + wchar_t* domain = malloc(domain_len * sizeof(wchar_t)); + if (name && domain && + LookupAccountSidW(NULL, user->User.Sid, name, &name_len, domain, &domain_len, &usage)) { + is_sshd = (_wcsicmp(name, L"sshd") == 0 && _wcsicmp(domain, L"NT SERVICE") == 0); } - free(buffer); + if (name) + free(name); + if (domain) + free(domain); } } - return user_name; + free(buffer); + return is_sshd; } /* @@ -1106,10 +1100,11 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, { PROCESS_INFORMATION pi; STARTUPINFOW si; - BOOL b; + BOOL b = FALSE; char *cmdline; wchar_t * cmdline_utf16 = NULL; int ret = -1; + if ((cmdline = build_commandline_string(cmd, argv, prepend_module_path)) == NULL) { errno = ENOMEM; goto cleanup; @@ -1125,7 +1120,7 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, si.hStdOutput = out; si.hStdError = err; si.dwFlags = STARTF_USESTDHANDLES; - + if (strstr(cmd, "sshd-session.exe") || strstr(cmd, "sshd-auth.exe")) { flags |= DETACHED_PROCESS; } @@ -1146,12 +1141,12 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, if (as_user) { debug3("spawning %ls as user", t); LPVOID lpEnvironment = NULL; - wchar_t* as_user_name = get_username_from_token(as_user); - if (as_user_name) { - if (wcsncmp(L"sshd", as_user_name, wcslen(L"sshd")) != 0) { /* Ignore any names that begin with the service name `sshd`. */ - b = CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); /* Load a user environment block inheriting the current context, thereby passing session state. */ - } - free(as_user_name); + if (!is_sshd_service_token(as_user)) { + /* Load the user's environment block (HKCU vars, USERPROFILE, etc.), + * inheriting the current context so session state set in + * sshd-session is preserved. Skipped for the NT SERVICE\sshd + * virtual account (sshd worker chain re-spawning itself). */ + CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); } if (lpEnvironment) { /* Pass the user environment block to the new process. */ b = CreateProcessAsUserW(as_user, NULL, t, NULL, NULL, TRUE, flags | CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &si, &pi); diff --git a/regress/pesterTests/UserEnvironment.Tests.ps1 b/regress/pesterTests/UserEnvironment.Tests.ps1 new file mode 100644 index 000000000000..1e301da0db21 --- /dev/null +++ b/regress/pesterTests/UserEnvironment.Tests.ps1 @@ -0,0 +1,131 @@ +If ($PSVersiontable.PSVersion.Major -le 2) {$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path} +Import-Module $PSScriptRoot\CommonUtils.psm1 -Force +Import-Module OpenSSHUtils -Force + +$tC = 1 +$tI = 0 +$suite = "userenvironment" + +Describe "E2E scenarios for user environment block" -Tags "CI" { + BeforeAll { + if ($OpenSSHTestInfo -eq $null) { + throw "`$OpenSSHTestInfo is null. Please run Set-OpenSSHTestEnvironment to set test environments." + } + + $server = $OpenSSHTestInfo["Target"] + $port = $OpenSSHTestInfo["Port"] + + $testDir = Join-Path $OpenSSHTestInfo["TestDataPath"] $suite + if (-not (Test-Path $testDir)) { + $null = New-Item $testDir -ItemType directory -Force -ErrorAction SilentlyContinue + } + + $script:envTestUser = $OpenSSHTestInfo["SshdUser"] + $script:envTestProfile = $OpenSSHTestInfo["SshdUserProfile"] + + $keypassphrase = "testpassword" + $script:envTestKey = Join-Path $testDir "sshd_user_envtest_ed25519" + Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue + ssh-keygen.exe -q -t ed25519 -f $script:envTestKey -N $keypassphrase + $envTestSshDir = Join-Path $script:envTestProfile .ssh + if (-not (Test-Path $envTestSshDir -PathType Container)) { + New-Item $envTestSshDir -ItemType Directory -Force -ErrorAction Stop | Out-Null + } + $script:envTestAuthKeys = Join-Path $envTestSshDir authorized_keys + Copy-Item "$($script:envTestKey).pub" $script:envTestAuthKeys -Force + Repair-AuthorizedKeyPermission -FilePath $script:envTestAuthKeys -confirm:$false + Add-PasswordSetting -Pass $keypassphrase + } + + AfterAll { + Remove-PasswordSetting + if ($script:envTestKey) { + Remove-Item -Path "$($script:envTestKey)*" -Force -ErrorAction SilentlyContinue + } + if ($script:envTestAuthKeys -and (Test-Path $script:envTestAuthKeys)) { + Remove-Item $script:envTestAuthKeys -Force -ErrorAction SilentlyContinue + } + } + + AfterEach { $tI++ } + + Context "$tC - User environment variables" { + BeforeAll { $tI = 1 } + AfterAll { $tC++ } + + It "$tC.$tI - USERNAME matches the connecting user" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERNAME% + "$o".Trim() | Should Be $script:envTestUser + } + + It "$tC.$tI - USERPROFILE points to the connecting user's profile" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE% + $remote = "$o".Trim() + $remote | Should Not BeNullOrEmpty + $remote | Should Not Match 'system32\\config\\systemprofile' + # Profile leaf is normally the user name, but Windows can suffix + # it (e.g. sshd_user.DESKTOP-XYZ.001) when the profile dir was + # recreated, so just check it starts with the user name. + ($remote -split '\\')[-1] | Should Match ("^" + [regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - HOMEDRIVE and HOMEPATH resolve to the user's profile" { + $hd = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEDRIVE%)".Trim() + $hp = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %HOMEPATH%)".Trim() + $up = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERPROFILE%)".Trim() + $hp | Should Not Match 'system32\\config\\systemprofile' + $hp | Should Not Match '^\\Windows' + $hp | Should Match ("^\\Users\\" + [regex]::Escape($script:envTestUser)) + $hd | Should Be $env:SystemDrive + ($hd + $hp) | Should Be $up + } + + It "$tC.$tI - APPDATA is populated for user" { + $ad = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %APPDATA%)".Trim() + $ad | Should Not Match 'system32\\config\\systemprofile' + $ad | Should Match 'AppData\\Roaming$' + $ad | Should Match ([regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - LOCALAPPDATA is populated for user" { + $la = "$(ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %LOCALAPPDATA%)".Trim() + $la | Should Not Match 'system32\\config\\systemprofile' + $la | Should Match 'AppData\\Local$' + $la | Should Match ([regex]::Escape($script:envTestUser)) + } + + It "$tC.$tI - USERDOMAIN equals the local computer name" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %USERDOMAIN% + $remote = "$o".Trim() + $remote | Should Not Be 'WORKGROUP' + $remote | Should Be $env:COMPUTERNAME + } + } + + Context "$tC - PATH variable" { + BeforeAll { + $tI = 1 + $script:pathMarker = "C:\sshtestmarker_$([guid]::NewGuid().ToString('N'))" + ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" ` + reg add HKCU\Environment /v Path /t REG_EXPAND_SZ /d $($script:pathMarker) /f | Out-Null + } + AfterAll { + ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" ` + reg delete HKCU\Environment /v Path /f | Out-Null + $tC++ + } + + It "$tC.$tI - contains both system and user entries" { + $o = ssh -p $port -i $script:envTestKey "$($script:envTestUser)@$server" echo %PATH% + $remote = "$o".Trim() + $segments = $remote -split ';' + $sysMatches = @($segments | Where-Object { $_ -match '[Ss]ystem32' }) + $markerMatches = @($segments | Where-Object { $_ -eq $script:pathMarker }) + $sysMatches.Count | Should BeGreaterThan 0 + $markerMatches.Count | Should BeGreaterThan 0 + $sysIndex = [array]::IndexOf($segments, $sysMatches[0]) + $markerIndex = [array]::IndexOf($segments, $script:pathMarker) + $sysIndex | Should BeLessThan $markerIndex + } + } +}