|
1 | 1 | // Copyright (c) Microsoft Corporation.
|
2 | 2 | // Licensed under the MIT license.
|
3 | 3 |
|
| 4 | +import { MSI, MSIConfiguration, MSIToken } from "./models"; |
4 | 5 | import {
|
5 | 6 | PipelineRequestOptions,
|
6 | 7 | createHttpHeaders,
|
7 | 8 | createPipelineRequest,
|
8 | 9 | } from "@azure/core-rest-pipeline";
|
9 |
| -import { GetTokenOptions } from "@azure/core-auth"; |
10 |
| -import { readFile } from "fs"; |
| 10 | + |
11 | 11 | import { AuthenticationError } from "../../errors";
|
12 |
| -import { credentialLogger } from "../../util/logging"; |
| 12 | +import { GetTokenOptions } from "@azure/core-auth"; |
13 | 13 | import { IdentityClient } from "../../client/identityClient";
|
14 |
| -import { mapScopesToResource } from "./utils"; |
15 |
| -import { MSI, MSIConfiguration, MSIToken } from "./models"; |
16 | 14 | import { azureArcAPIVersion } from "./constants";
|
| 15 | +import { credentialLogger } from "../../util/logging"; |
| 16 | +import fs from "node:fs"; |
| 17 | +import { mapScopesToResource } from "./utils"; |
17 | 18 |
|
18 | 19 | const msiName = "ManagedIdentityCredential - Azure Arc MSI";
|
19 | 20 | const logger = credentialLogger(msiName);
|
@@ -60,21 +61,6 @@ function prepareRequestOptions(
|
60 | 61 | });
|
61 | 62 | }
|
62 | 63 |
|
63 |
| -/** |
64 |
| - * Retrieves the file contents at the given path using promises. |
65 |
| - * Useful since `fs`'s readFileSync locks the thread, and to avoid extra dependencies. |
66 |
| - */ |
67 |
| -function readFileAsync(path: string, options: { encoding: BufferEncoding }): Promise<string> { |
68 |
| - return new Promise((resolve, reject) => |
69 |
| - readFile(path, options, (err, data) => { |
70 |
| - if (err) { |
71 |
| - reject(err); |
72 |
| - } |
73 |
| - resolve(data); |
74 |
| - }), |
75 |
| - ); |
76 |
| -} |
77 |
| - |
78 | 64 | /**
|
79 | 65 | * Does a request to the authentication provider that results in a file path.
|
80 | 66 | */
|
@@ -103,6 +89,50 @@ async function filePathRequest(
|
103 | 89 | }
|
104 | 90 | }
|
105 | 91 |
|
| 92 | +export function platformToFilePath(): string { |
| 93 | + switch (process.platform) { |
| 94 | + case "win32": |
| 95 | + if (!process.env.PROGRAMDATA) { |
| 96 | + throw new Error(`${msiName}: PROGRAMDATA environment variable has no value.`); |
| 97 | + } |
| 98 | + return `${process.env.PROGRAMDATA}\\AzureConnectedMachineAgent\\Tokens`; |
| 99 | + case "linux": |
| 100 | + return "/var/opt/azcmagent/tokens"; |
| 101 | + default: |
| 102 | + throw new Error(`${msiName}: Unsupported platform ${process.platform}.`); |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +/** |
| 107 | + * Validates that a given Azure Arc MSI file path is valid for use. |
| 108 | + * |
| 109 | + * A valid file will: |
| 110 | + * 1. Be in the expected path for the current platform. |
| 111 | + * 2. Have a `.key` extension. |
| 112 | + * 3. Be at most 4096 bytes in size. |
| 113 | + */ |
| 114 | +export function validateKeyFile(filePath?: string): asserts filePath is string { |
| 115 | + if (!filePath) { |
| 116 | + throw new Error(`${msiName}: Failed to find the token file.`); |
| 117 | + } |
| 118 | + |
| 119 | + if (!filePath.endsWith(".key")) { |
| 120 | + throw new Error(`${msiName}: unexpected file path from HIMDS service: ${filePath}.`); |
| 121 | + } |
| 122 | + |
| 123 | + const expectedPath = platformToFilePath(); |
| 124 | + if (!filePath.startsWith(expectedPath)) { |
| 125 | + throw new Error(`${msiName}: unexpected file path from HIMDS service: ${filePath}.`); |
| 126 | + } |
| 127 | + |
| 128 | + const stats = fs.statSync(filePath); |
| 129 | + if (stats.size > 4096) { |
| 130 | + throw new Error( |
| 131 | + `${msiName}: The file at ${filePath} is larger than expected at ${stats.size} bytes.`, |
| 132 | + ); |
| 133 | + } |
| 134 | +} |
| 135 | + |
106 | 136 | /**
|
107 | 137 | * Defines how to determine whether the Azure Arc MSI is available, and also how to retrieve a token from the Azure Arc MSI.
|
108 | 138 | */
|
@@ -150,12 +180,9 @@ export const arcMsi: MSI = {
|
150 | 180 | };
|
151 | 181 |
|
152 | 182 | const filePath = await filePathRequest(identityClient, requestOptions);
|
| 183 | + validateKeyFile(filePath); |
153 | 184 |
|
154 |
| - if (!filePath) { |
155 |
| - throw new Error(`${msiName}: Failed to find the token file.`); |
156 |
| - } |
157 |
| - |
158 |
| - const key = await readFileAsync(filePath, { encoding: "utf-8" }); |
| 185 | + const key = await fs.promises.readFile(filePath, { encoding: "utf-8" }); |
159 | 186 | requestOptions.headers?.set("Authorization", `Basic ${key}`);
|
160 | 187 |
|
161 | 188 | const request = createPipelineRequest({
|
|
0 commit comments