Skip to content

Commit

Permalink
Managed Identity - Added file-based detection for Azure Arc (#7225)
Browse files Browse the repository at this point in the history
If an Azure Arc Managed Identity's "IDENTITY_ENDPOINT" and
"IMDS_ENDPOINT" environment variables are undefined, the Managed
Identity can still be determined to be an instance of Azure Arc if it
has a "himds" executable in the specified path for Windows or Linux.

Manual tests were successful on both Windows and Linux.
  • Loading branch information
Robbie-Microsoft committed Aug 9, 2024
1 parent 4592e9f commit 630605c
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Added file-based detection for Azure Arc",
"packageName": "@azure/msal-node",
"email": "rginsburg@microsoft.com",
"dependentChangeType": "patch"
}
105 changes: 80 additions & 25 deletions lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,36 @@ import {
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
} from "../../utils/Constants";
import { NodeStorage } from "../../cache/NodeStorage";
import { readFileSync, statSync } from "fs";
import {
accessSync,
constants as fsConstants,
readFileSync,
statSync,
} from "fs";
import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse";
import { ManagedIdentityId } from "../../config/ManagedIdentityId";
import path from "path";

export const ARC_API_VERSION: string = "2019-11-01";
export const DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT: string =
"http://127.0.0.1:40342/metadata/identity/oauth2/token";
const HIMDS_EXECUTABLE_HELPER_STRING = "N/A: himds executable exists";

type FilePathMap = {
win32: string;
linux: string;
};

export const SUPPORTED_AZURE_ARC_PLATFORMS = {
export const SUPPORTED_AZURE_ARC_PLATFORMS: FilePathMap = {
win32: `${process.env["ProgramData"]}\\AzureConnectedMachineAgent\\Tokens\\`,
linux: "/var/opt/azcmagent/tokens/",
};

export const AZURE_ARC_FILE_DETECTION: FilePathMap = {
win32: `${process.env["ProgramFiles"]}\\AzureConnectedMachineAgent\\himds.exe`,
linux: "/opt/azcmagent/bin/himds",
};

/**
* Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs
*/
Expand All @@ -64,13 +82,38 @@ export class AzureArc extends BaseManagedIdentitySource {
}

public static getEnvironmentVariables(): Array<string | undefined> {
const identityEndpoint: string | undefined =
let identityEndpoint: string | undefined =
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
];
const imdsEndpoint: string | undefined =
let imdsEndpoint: string | undefined =
process.env[ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT];

// if either of the identity or imds endpoints are undefined, check if the himds executable exists
if (!identityEndpoint || !imdsEndpoint) {
// get the expected Windows or Linux file path of the himds executable
const fileDetectionPath: string =
AZURE_ARC_FILE_DETECTION[process.platform as keyof FilePathMap];
try {
/*
* check if the himds executable exists and its permissions allow it to be read
* returns undefined if true, throws an error otherwise
*/
accessSync(
fileDetectionPath,
fsConstants.F_OK | fsConstants.R_OK
);

identityEndpoint = DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT;
imdsEndpoint = HIMDS_EXECUTABLE_HELPER_STRING;
} catch (err) {
/*
* do nothing
* accessSync returns undefined on success, and throws an error on failure
*/
}
}

return [identityEndpoint, imdsEndpoint];
}

Expand All @@ -84,36 +127,46 @@ export class AzureArc extends BaseManagedIdentitySource {
const [identityEndpoint, imdsEndpoint] =
AzureArc.getEnvironmentVariables();

// if either of the identity or imds endpoints are undefined, this MSI provider is unavailable.
// if either of the identity or imds endpoints are undefined (even after himds file detection)
if (!identityEndpoint || !imdsEndpoint) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is unavailable because one or both of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT}' environment variables are not defined.`
`[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is unavailable through environment variables because one or both of '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT}' are not defined. ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is also unavailable through file detection.`
);

return null;
}

const validatedIdentityEndpoint: string =
// check if the imds endpoint is set to the default for file detection
if (imdsEndpoint === HIMDS_EXECUTABLE_HELPER_STRING) {
logger.info(
`[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is available through file detection. Defaulting to known ${ManagedIdentitySourceNames.AZURE_ARC} endpoint: ${DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`
);
} else {
// otherwise, both the identity and imds endpoints are defined without file detection; validate them

const validatedIdentityEndpoint: string =
AzureArc.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
identityEndpoint,
ManagedIdentitySourceNames.AZURE_ARC,
logger
);
// remove trailing slash
validatedIdentityEndpoint.endsWith("/")
? validatedIdentityEndpoint.slice(0, -1)
: validatedIdentityEndpoint;

AzureArc.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
identityEndpoint,
ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT,
imdsEndpoint,
ManagedIdentitySourceNames.AZURE_ARC,
logger
);
// remove trailing slash
validatedIdentityEndpoint.endsWith("/")
? validatedIdentityEndpoint.slice(0, -1)
: validatedIdentityEndpoint;

AzureArc.getValidatedEnvVariableUrlString(
ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT,
imdsEndpoint,
ManagedIdentitySourceNames.AZURE_ARC,
logger
);

logger.info(
`[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.AZURE_ARC} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`
);
logger.info(
`[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.AZURE_ARC} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`
);
}

if (
managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
Expand Down Expand Up @@ -186,9 +239,11 @@ export class AzureArc extends BaseManagedIdentitySource {
);
}

// get the expected Windows or Linux file path)
// get the expected Windows or Linux file path
const expectedSecretFilePath: string =
SUPPORTED_AZURE_ARC_PLATFORMS[process.platform as string];
SUPPORTED_AZURE_ARC_PLATFORMS[
process.platform as keyof FilePathMap
];

// throw an error if the file in the file path is not a .key file
const fileName: string = path.basename(secretFilePath);
Expand Down
65 changes: 65 additions & 0 deletions lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jest.mock("fs");

describe("Acquires a token successfully via an Azure Arc Managed Identity", () => {
let originalPlatform: string;
let accessSyncSpy: jest.SpyInstance;

beforeAll(() => {
process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] =
Expand All @@ -56,6 +57,14 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () =
Object.defineProperty(process, "platform", {
value: "linux",
});

accessSyncSpy = jest
.spyOn(fs, "accessSync")
// returns undefined when the himds file exists and its permissions allow it to be read
// otherwise, throws an error
.mockImplementation(() => {
throw new Error();
});
});

afterAll(() => {
Expand Down Expand Up @@ -110,6 +119,62 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () =
);
});

test("acquires a token when both/either the identityEndpoint and/or imdsEndpoint environment variables are undefined, and the himds executable exists and its permissions allow it to be read", async () => {
// delete the environment variables so the himds executable is checked
delete process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
];
delete process.env[
ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT
];
// delete value cached from getManagedIdentitySource() in the beforeEach
delete ManagedIdentityClient["sourceName"];

// MI source will not be Azure Arc yet, since the environment variables are undefined,
// and accessSyncSpy still returns an error
// (meaning either the himds file doesn't exists or its permissions don't allow it to be read)
expect(
managedIdentityApplication.getManagedIdentitySource()
).not.toBe(ManagedIdentitySourceNames.AZURE_ARC);
// delete value cached from getManagedIdentitySource() directly above
delete ManagedIdentityClient["sourceName"];

// returns undefined when the himds file exists and its permissions allow it to be read
// otherwise, throws an error
accessSyncSpy.mockImplementationOnce(() => {
return undefined;
});

expect(managedIdentityApplication.getManagedIdentitySource()).toBe(
ManagedIdentitySourceNames.AZURE_ARC
);

// returns undefined when the himds file exists and its permissions allow it to be read
// otherwise, throws an error
accessSyncSpy.mockImplementationOnce(() => {
return undefined;
});
const networkManagedIdentityResult: AuthenticationResult =
await managedIdentityApplication.acquireToken(
managedIdentityRequestParams
);
expect(networkManagedIdentityResult.fromCache).toBe(false);

expect(networkManagedIdentityResult.accessToken).toEqual(
DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken
);

// one for each call to getManagedIdentitySource() + one for the acquireToken call
expect(accessSyncSpy).toHaveBeenCalledTimes(3);

// reset the environment variables to expected values for Azure Arc tests
process.env[
ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
] = "fake_IDENTITY_ENDPOINT";
process.env[ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT] =
"fake_IMDS_ENDPOINT";
});

test("returns an already acquired token from the cache", async () => {
const networkManagedIdentityResult: AuthenticationResult =
await managedIdentityApplication.acquireToken({
Expand Down

0 comments on commit 630605c

Please sign in to comment.