Skip to content

Commit 925ea6c

Browse files
authored
Handle error when pwsh does not exist language agnostic (Azure#41910)
1 parent 6a9de22 commit 925ea6c

File tree

4 files changed

+12
-6
lines changed

4 files changed

+12
-6
lines changed

sdk/identity/Azure.Identity/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Breaking Changes
88

99
### Bugs Fixed
10+
- `AzurePowerShellCredential` now handles the case where it falls back to legacy powershell without relying on the error message string.
1011

1112
### Other Changes
1213

sdk/identity/Azure.Identity/src/Credentials/AzurePowerShellCredential.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ private async ValueTask<AccessToken> RequestAzurePowerShellAccessTokenAsync(bool
159159
try
160160
{
161161
output = async ? await processRunner.RunAsync().ConfigureAwait(false) : processRunner.Run();
162-
CheckForErrors(output);
162+
CheckForErrors(output, processRunner.ExitCode);
163163
ValidateResult(output);
164164
}
165165
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
@@ -168,7 +168,7 @@ private async ValueTask<AccessToken> RequestAzurePowerShellAccessTokenAsync(bool
168168
}
169169
catch (InvalidOperationException exception)
170170
{
171-
CheckForErrors(exception.Message);
171+
CheckForErrors(exception.Message, processRunner.ExitCode);
172172
if (_isChainedCredential)
173173
{
174174
throw new CredentialUnavailableException($"{AzurePowerShellFailedError} {exception.Message}");
@@ -181,9 +181,10 @@ private async ValueTask<AccessToken> RequestAzurePowerShellAccessTokenAsync(bool
181181
return DeserializeOutput(output);
182182
}
183183

184-
private static void CheckForErrors(string output)
184+
private static void CheckForErrors(string output, int exitCode)
185185
{
186-
bool noPowerShell = (output.IndexOf("not found", StringComparison.OrdinalIgnoreCase) != -1 ||
186+
int notFoundExitCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 9009 : 127;
187+
bool noPowerShell = (exitCode == notFoundExitCode || output.IndexOf("not found", StringComparison.OrdinalIgnoreCase) != -1 ||
187188
output.IndexOf("is not recognized", StringComparison.OrdinalIgnoreCase) != -1) &&
188189
// If the error contains AADSTS, this should be treated as a general error to be bubbled to the user
189190
output.IndexOf("AADSTS", StringComparison.OrdinalIgnoreCase) == -1;
@@ -264,7 +265,7 @@ private void GetFileNameAndArguments(string resource, string tenantId, out strin
264265
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
265266
{
266267
fileName = Path.Combine(DefaultWorkingDirWindows, "cmd.exe");
267-
argument = $"/d /c \"{powershellExe} \"{commandBase64}\" \"";
268+
argument = $"/d /c \"{powershellExe} \"{commandBase64}\" \" & exit";
268269
}
269270
else
270271
{

sdk/identity/Azure.Identity/src/ProcessRunner.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class ProcessRunner : IDisposable
2323
private readonly CancellationTokenSource _timeoutCts;
2424
private CancellationTokenRegistration _ctRegistration;
2525
private bool _logPII;
26+
public int ExitCode => _process.ExitCode;
2627

2728
public ProcessRunner(IProcess process, TimeSpan timeout, bool logPII, CancellationToken cancellationToken)
2829
{

sdk/identity/Azure.Identity/tests/AzurePowerShellCredentialsTests.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ private static IEnumerable<object[]> FallBackErrorScenarios()
119119
yield return new object[] { "'pwsh' is not recognized", AzurePowerShellCredential.PowerShellNotInstalledError };
120120
yield return new object[] { "pwsh: command not found", AzurePowerShellCredential.PowerShellNotInstalledError };
121121
yield return new object[] { "pwsh: not found", AzurePowerShellCredential.PowerShellNotInstalledError };
122+
yield return new object[] { "foo bar", AzurePowerShellCredential.PowerShellNotInstalledError };
122123
}
123124

124125
[Test]
@@ -127,7 +128,9 @@ public void AuthenticateWithAzurePowerShellCredential_FallBackErrorScenarios(str
127128
{
128129
// This will require two processes on Windows and one on other platforms
129130
// Purposefully stripping out the second process to ensure any attempt to fallback is caught on non-Windows
130-
TestProcess[] testProcesses = new TestProcess[] { new TestProcess { Error = errorMessage }, new TestProcess { Error = errorMessage } };
131+
int exitCode = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 9009 : 127;
132+
133+
TestProcess[] testProcesses = new TestProcess[] { new TestProcess { Error = errorMessage, CodeOnExit = exitCode }, new TestProcess { Error = errorMessage, CodeOnExit = exitCode } };
131134
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
132135
testProcesses = new TestProcess[] { testProcesses[0] };
133136

0 commit comments

Comments
 (0)