UNPKG

ruchy-syntax-tools

Version:

Comprehensive syntax highlighting and language support for the Ruchy programming language

196 lines 8.73 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { checkTenantId, processMultiTenantRequest, resolveAdditionallyAllowedTenantIds, } from "../util/tenantIdUtils.js"; import { credentialLogger, formatError, formatSuccess } from "../util/logging.js"; import { ensureValidScopeForDevTimeCreds, getScopeResource } from "../util/scopeUtils.js"; import { CredentialUnavailableError } from "../errors.js"; import child_process from "child_process"; import { tracingClient } from "../util/tracing.js"; import { checkSubscription } from "../util/subscriptionUtils.js"; const logger = credentialLogger("AzureCliCredential"); /** * Mockable reference to the CLI credential cliCredentialFunctions * @internal */ export const cliCredentialInternals = { /** * @internal */ getSafeWorkingDir() { if (process.platform === "win32") { let systemRoot = process.env.SystemRoot || process.env["SYSTEMROOT"]; if (!systemRoot) { logger.getToken.warning("The SystemRoot environment variable is not set. This may cause issues when using the Azure CLI credential."); systemRoot = "C:\\Windows"; } return systemRoot; } else { return "/bin"; } }, /** * Gets the access token from Azure CLI * @param resource - The resource to use when getting the token * @internal */ async getAzureCliAccessToken(resource, tenantId, subscription, timeout) { let tenantSection = []; let subscriptionSection = []; if (tenantId) { tenantSection = ["--tenant", tenantId]; } if (subscription) { // Add quotes around the subscription to handle subscriptions with spaces subscriptionSection = ["--subscription", `"${subscription}"`]; } return new Promise((resolve, reject) => { try { const args = [ "account", "get-access-token", "--output", "json", "--resource", resource, ...tenantSection, ...subscriptionSection, ]; const command = ["az", ...args].join(" "); child_process.exec(command, { cwd: cliCredentialInternals.getSafeWorkingDir(), timeout }, (error, stdout, stderr) => { resolve({ stdout: stdout, stderr: stderr, error }); }); } catch (err) { reject(err); } }); }, }; /** * This credential will use the currently logged-in user login information * via the Azure CLI ('az') commandline tool. * To do so, it will read the user access token and expire time * with Azure CLI command "az account get-access-token". */ export class AzureCliCredential { tenantId; additionallyAllowedTenantIds; timeout; subscription; /** * Creates an instance of the {@link AzureCliCredential}. * * To use this credential, ensure that you have already logged * in via the 'az' tool using the command "az login" from the commandline. * * @param options - Options, to optionally allow multi-tenant requests. */ constructor(options) { if (options?.tenantId) { checkTenantId(logger, options?.tenantId); this.tenantId = options?.tenantId; } if (options?.subscription) { checkSubscription(logger, options?.subscription); this.subscription = options?.subscription; } this.additionallyAllowedTenantIds = resolveAdditionallyAllowedTenantIds(options?.additionallyAllowedTenants); this.timeout = options?.processTimeoutInMs; } /** * Authenticates with Microsoft Entra ID and returns an access token if successful. * If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure. * * @param scopes - The list of scopes for which the token will have access. * @param options - The options used to configure any requests this * TokenCredential implementation might make. */ async getToken(scopes, options = {}) { const tenantId = processMultiTenantRequest(this.tenantId, options, this.additionallyAllowedTenantIds); if (tenantId) { checkTenantId(logger, tenantId); } if (this.subscription) { checkSubscription(logger, this.subscription); } const scope = typeof scopes === "string" ? scopes : scopes[0]; logger.getToken.info(`Using the scope ${scope}`); return tracingClient.withSpan(`${this.constructor.name}.getToken`, options, async () => { try { ensureValidScopeForDevTimeCreds(scope, logger); const resource = getScopeResource(scope); const obj = await cliCredentialInternals.getAzureCliAccessToken(resource, tenantId, this.subscription, this.timeout); const specificScope = obj.stderr?.match("(.*)az login --scope(.*)"); const isLoginError = obj.stderr?.match("(.*)az login(.*)") && !specificScope; const isNotInstallError = obj.stderr?.match("az:(.*)not found") || obj.stderr?.startsWith("'az' is not recognized"); if (isNotInstallError) { const error = new CredentialUnavailableError("Azure CLI could not be found. Please visit https://aka.ms/azure-cli for installation instructions and then, once installed, authenticate to your Azure account using 'az login'."); logger.getToken.info(formatError(scopes, error)); throw error; } if (isLoginError) { const error = new CredentialUnavailableError("Please run 'az login' from a command prompt to authenticate before using this credential."); logger.getToken.info(formatError(scopes, error)); throw error; } try { const responseData = obj.stdout; const response = this.parseRawResponse(responseData); logger.getToken.info(formatSuccess(scopes)); return response; } catch (e) { if (obj.stderr) { throw new CredentialUnavailableError(obj.stderr); } throw e; } } catch (err) { const error = err.name === "CredentialUnavailableError" ? err : new CredentialUnavailableError(err.message || "Unknown error while trying to retrieve the access token"); logger.getToken.info(formatError(scopes, error)); throw error; } }); } /** * Parses the raw JSON response from the Azure CLI into a usable AccessToken object * * @param rawResponse - The raw JSON response from the Azure CLI * @returns An access token with the expiry time parsed from the raw response * * The expiryTime of the credential's access token, in milliseconds, is calculated as follows: * * When available, expires_on (introduced in Azure CLI v2.54.0) will be preferred. Otherwise falls back to expiresOn. */ parseRawResponse(rawResponse) { const response = JSON.parse(rawResponse); const token = response.accessToken; // if available, expires_on will be a number representing seconds since epoch. // ensure it's a number or NaN let expiresOnTimestamp = Number.parseInt(response.expires_on, 10) * 1000; if (!isNaN(expiresOnTimestamp)) { logger.getToken.info("expires_on is available and is valid, using it"); return { token, expiresOnTimestamp, tokenType: "Bearer", }; } // fallback to the older expiresOn - an RFC3339 date string expiresOnTimestamp = new Date(response.expiresOn).getTime(); // ensure expiresOn is well-formatted if (isNaN(expiresOnTimestamp)) { throw new CredentialUnavailableError(`Unexpected response from Azure CLI when getting token. Expected "expiresOn" to be a RFC3339 date string. Got: "${response.expiresOn}"`); } return { token, expiresOnTimestamp, tokenType: "Bearer", }; } } //# sourceMappingURL=azureCliCredential.js.map