Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Managed Identity - Added file-based detection for Azure Arc #7225

Merged
merged 8 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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";
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved

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> {
bgavrilMS marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add logging statements for these logic branches

Copy link
Collaborator Author

@Robbie-Microsoft Robbie-Microsoft Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logging for this logic exists already on line 142

/*
* 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) {
/*
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
* 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
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(() => {
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
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[
Robbie-Microsoft marked this conversation as resolved.
Show resolved Hide resolved
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
Loading