[Identity] ManagedIdentityCredential fix (#13919)

This pull request makes the ManagedIdentityCredential:

- Treat unreachable host as we were treating unreachable network.
  - This should have been done before, but we didn't catch it.
  - I couldn't find any other error that seemed relevant to me, using this as reference: [link](606df7c4e7/deps/uv/src/win/error.c).
- Treat errors with undefined status code as if the credential was unavailable, to avoid breaking the chained token credentials in that case.
- Add tests (turns out that some where being skipped by mistake!)
- Add more comments to improve reading.

This PR:
Fixes #13894
This commit is contained in:
Daniel Rodríguez 2021-02-23 15:55:02 -05:00 коммит произвёл GitHub
Родитель 2930b04b9f
Коммит 39d7184595
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 97 добавлений и 11 удалений

Просмотреть файл

@ -7,6 +7,7 @@
- `DefaultAzureCredential`'s implementation for browsers was simplified to throw a simple error instead of trying credentials that were already not supported for the browser.
- Breaking Change: `InteractiveBrowserCredential` for the browser now requires the client ID to be provided.
- Documentation was added to elaborate on how to configure an AAD application to support `InteractiveBrowserCredential`.
- Bug fix: `ManagedIdentityCredential` now also properly handles `EHOSTUNREACH` errors. Fixes issue [13894](https://github.com/Azure/azure-sdk-for-js/issues/13894).
## 1.2.4-beta.1 (2021-02-12)

Просмотреть файл

@ -38,7 +38,8 @@ export function nodeConfig(test = false) {
baseConfig.input = [
"dist-esm/test/public/*.spec.js",
"dist-esm/test/public/node/*.spec.js",
"dist-esm/test/internal/*.spec.js"
"dist-esm/test/internal/*.spec.js",
"dist-esm/test/internal/node/*.spec.js"
];
baseConfig.plugins.unshift(multiEntry({ exports: false }));
@ -97,7 +98,8 @@ export function browserConfig(test = false) {
baseConfig.input = [
"dist-esm/test/public/*.spec.js",
"dist-esm/test/public/browser/*.spec.js",
"dist-esm/test/internal/*.spec.js"
"dist-esm/test/internal/*.spec.js",
"dist-esm/test/internal/browser/*.spec.js"
];
baseConfig.plugins.unshift(multiEntry({ exports: false }));
baseConfig.output.file = "test-browser/index.js";

Просмотреть файл

@ -196,7 +196,20 @@ export class ManagedIdentityCredential implements TokenCredential {
message: err.message
});
// If either the network is unreachable,
// we can safely assume the credential is unavailable.
if (err.code === "ENETUNREACH") {
const error = new CredentialUnavailable(
"ManagedIdentityCredential is unavailable. Network unreachable."
);
logger.getToken.info(formatError(scopes, error));
throw error;
}
// If either the host was unreachable,
// we can safely assume the credential is unavailable.
if (err.code === "EHOSTUNREACH") {
const error = new CredentialUnavailable(
"ManagedIdentityCredential is unavailable. No managed identity endpoint found."
);
@ -213,6 +226,15 @@ export class ManagedIdentityCredential implements TokenCredential {
);
}
// If the error has no status code, we can assume there was no available identity.
// This will throw silently during any ChainedTokenCredential.
if (err.statusCode === undefined) {
throw new CredentialUnavailable(
`ManagedIdentityCredential authentication failed. Message ${err.message}`
);
}
// Any other error should break the chain.
throw new AuthenticationError(err.statusCode, {
error: "ManagedIdentityCredential authentication failed.",
error_description: err.message

Просмотреть файл

@ -14,7 +14,8 @@ import {
import * as coreHttp from "@azure/core-http";
export interface MockAuthResponse {
status: number;
status?: number;
error?: RestError;
headers?: HttpHeaders;
parsedBody?: any;
bodyAsText?: string;
@ -27,7 +28,7 @@ export interface MockAuthHttpClientOptions {
export class MockAuthHttpClient implements HttpClient {
private authResponses: MockAuthResponse[] = [];
private currentResponse: number = 0;
private currentResponseIndex: number = 0;
private mockTimeout: boolean;
public tokenCredentialOptions: ClientCertificateCredentialOptions;
@ -76,13 +77,22 @@ export class MockAuthHttpClient implements HttpClient {
throw new Error("The number of requests has exceeded the number of authResponses");
}
const authResponse = this.authResponses[this.currentResponseIndex];
if (authResponse.error) {
this.currentResponseIndex++;
throw authResponse.error;
}
const response = {
request: httpRequest,
headers: this.authResponses[this.currentResponse].headers || new HttpHeaders(),
...this.authResponses[this.currentResponse]
headers: authResponse.headers || new HttpHeaders(),
status: authResponse.status || 200,
parsedBody: authResponse.parsedBody,
bodyAsText: authResponse.bodyAsText
};
this.currentResponse++;
this.currentResponseIndex++;
return response;
}
}

Просмотреть файл

@ -9,7 +9,7 @@ import {
imdsApiVersion
} from "../../../src/credentials/managedIdentityCredential/constants";
import { MockAuthHttpClient, MockAuthHttpClientOptions, assertRejects } from "../../authTestUtils";
import { WebResource, AccessToken, HttpHeaders } from "@azure/core-http";
import { WebResource, AccessToken, HttpHeaders, RestError } from "@azure/core-http";
import { OAuthErrorResponse } from "../../../src/client/errors";
interface AuthRequestDetails {
@ -83,7 +83,24 @@ describe("ManagedIdentityCredential", function() {
}
});
it("returns error when ManagedIdentityCredential authentication failed", async function() {
it("returns error when no MSI is available", async function() {
process.env.AZURE_CLIENT_ID = "errclient";
const imdsError: RestError = new RestError("Request Timeout", "REQUEST_SEND_ERROR", 408);
const mockHttpClient = new MockAuthHttpClient({
authResponse: [{ error: imdsError }]
});
const credential = new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID, {
...mockHttpClient.tokenCredentialOptions
});
await assertRejects(
credential.getToken("scopes"),
(error: AuthenticationError) => error.message.indexOf("No MSI credential available") > -1
);
});
it("an unexpected error bubbles all the way up", async function() {
process.env.AZURE_CLIENT_ID = "errclient";
const errResponse: OAuthErrorResponse = {
@ -92,7 +109,41 @@ describe("ManagedIdentityCredential", function() {
};
const mockHttpClient = new MockAuthHttpClient({
authResponse: [{ status: 400, parsedBody: errResponse }]
authResponse: [{ status: 200 }, { status: 500, parsedBody: errResponse }]
});
const credential = new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID, {
...mockHttpClient.tokenCredentialOptions
});
await assertRejects(
credential.getToken("scopes"),
(error: AuthenticationError) => error.message.indexOf(errResponse.error) > -1
);
});
it("returns expected error when the network was unreachable", async function() {
process.env.AZURE_CLIENT_ID = "errclient";
const netError: RestError = new RestError("Request Timeout", "ENETUNREACH", 408);
const mockHttpClient = new MockAuthHttpClient({
authResponse: [{ status: 200 }, { error: netError }]
});
const credential = new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID, {
...mockHttpClient.tokenCredentialOptions
});
await assertRejects(
credential.getToken("scopes"),
(error: AuthenticationError) => error.message.indexOf("Network unreachable.") > -1
);
});
it("returns expected error when the host was unreachable", async function() {
process.env.AZURE_CLIENT_ID = "errclient";
const hostError: RestError = new RestError("Request Timeout", "EHOSTUNREACH", 408);
const mockHttpClient = new MockAuthHttpClient({
authResponse: [{ status: 200 }, { error: hostError }]
});
const credential = new ManagedIdentityCredential(process.env.AZURE_CLIENT_ID, {
@ -101,7 +152,7 @@ describe("ManagedIdentityCredential", function() {
await assertRejects(
credential.getToken("scopes"),
(error: AuthenticationError) =>
error.errorResponse.error.indexOf("ManagedIdentityCredential authentication failed.") > -1
error.message.indexOf("No managed identity endpoint found.") > -1
);
});