ruchy-syntax-tools
Version:
Comprehensive syntax highlighting and language support for the Ruchy programming language
245 lines (238 loc) • 9.89 kB
JavaScript
// 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 { processUtils } from "../util/processUtils.js";
import { tracingClient } from "../util/tracing.js";
const logger = credentialLogger("AzurePowerShellCredential");
const isWindows = process.platform === "win32";
/**
* Returns a platform-appropriate command name by appending ".exe" on Windows.
*
* @internal
*/
export function formatCommand(commandName) {
if (isWindows) {
return `${commandName}.exe`;
}
else {
return commandName;
}
}
/**
* Receives a list of commands to run, executes them, then returns the outputs.
* If anything fails, an error is thrown.
* @internal
*/
async function runCommands(commands, timeout) {
const results = [];
for (const command of commands) {
const [file, ...parameters] = command;
const result = (await processUtils.execFile(file, parameters, {
encoding: "utf8",
timeout,
}));
results.push(result);
}
return results;
}
/**
* Known PowerShell errors
* @internal
*/
export const powerShellErrors = {
login: "Run Connect-AzAccount to login",
installed: "The specified module 'Az.Accounts' with version '2.2.0' was not loaded because no valid module file was found in any module directory",
};
/**
* Messages to use when throwing in this credential.
* @internal
*/
export const powerShellPublicErrorMessages = {
login: "Please run 'Connect-AzAccount' from PowerShell to authenticate before using this credential.",
installed: `The 'Az.Account' module >= 2.2.0 is not installed. Install the Azure Az PowerShell module with: "Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force".`,
troubleshoot: `To troubleshoot, visit https://aka.ms/azsdk/js/identity/powershellcredential/troubleshoot.`,
};
// PowerShell Azure User not logged in error check.
const isLoginError = (err) => err.message.match(`(.*)${powerShellErrors.login}(.*)`);
// Az Module not Installed in Azure PowerShell check.
const isNotInstalledError = (err) => err.message.match(powerShellErrors.installed);
/**
* The PowerShell commands to be tried, in order.
*
* @internal
*/
export const commandStack = [formatCommand("pwsh")];
if (isWindows) {
commandStack.push(formatCommand("powershell"));
}
/**
* This credential will use the currently logged-in user information from the
* Azure PowerShell module. To do so, it will read the user access token and
* expire time with Azure PowerShell command `Get-AzAccessToken -ResourceUrl {ResourceScope}`
*/
export class AzurePowerShellCredential {
tenantId;
additionallyAllowedTenantIds;
timeout;
/**
* Creates an instance of the {@link AzurePowerShellCredential}.
*
* To use this credential:
* - Install the Azure Az PowerShell module with:
* `Install-Module -Name Az -Scope CurrentUser -Repository PSGallery -Force`.
* - You have already logged in to Azure PowerShell using the command
* `Connect-AzAccount` from the command line.
*
* @param options - Options, to optionally allow multi-tenant requests.
*/
constructor(options) {
if (options?.tenantId) {
checkTenantId(logger, options?.tenantId);
this.tenantId = options?.tenantId;
}
this.additionallyAllowedTenantIds = resolveAdditionallyAllowedTenantIds(options?.additionallyAllowedTenants);
this.timeout = options?.processTimeoutInMs;
}
/**
* Gets the access token from Azure PowerShell
* @param resource - The resource to use when getting the token
*/
async getAzurePowerShellAccessToken(resource, tenantId, timeout) {
// Clone the stack to avoid mutating it while iterating
for (const powerShellCommand of [...commandStack]) {
try {
await runCommands([[powerShellCommand, "/?"]], timeout);
}
catch (e) {
// Remove this credential from the original stack so that we don't try it again.
commandStack.shift();
continue;
}
const results = await runCommands([
[
powerShellCommand,
"-NoProfile",
"-NonInteractive",
"-Command",
`
$tenantId = "${tenantId ?? ""}"
$m = Import-Module Az.Accounts -MinimumVersion 2.2.0 -PassThru
$useSecureString = $m.Version -ge [version]'2.17.0' -and $m.Version -lt [version]'5.0.0'
$params = @{
ResourceUrl = "${resource}"
}
if ($tenantId.Length -gt 0) {
$params["TenantId"] = $tenantId
}
if ($useSecureString) {
$params["AsSecureString"] = $true
}
$token = Get-AzAccessToken @params
$result = New-Object -TypeName PSObject
$result | Add-Member -MemberType NoteProperty -Name ExpiresOn -Value $token.ExpiresOn
if ($token.Token -is [System.Security.SecureString]) {
if ($PSVersionTable.PSVersion.Major -lt 7) {
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.Token)
try {
$result | Add-Member -MemberType NoteProperty -Name Token -Value ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr))
}
finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
}
}
else {
$result | Add-Member -MemberType NoteProperty -Name Token -Value ($token.Token | ConvertFrom-SecureString -AsPlainText)
}
}
else {
$result | Add-Member -MemberType NoteProperty -Name Token -Value $token.Token
}
Write-Output (ConvertTo-Json $result)
`,
],
]);
const result = results[0];
return parseJsonToken(result);
}
throw new Error(`Unable to execute PowerShell. Ensure that it is installed in your system`);
}
/**
* Authenticates with Microsoft Entra ID and returns an access token if successful.
* If the authentication cannot be performed through PowerShell, a {@link CredentialUnavailableError} will be thrown.
*
* @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 = {}) {
return tracingClient.withSpan(`${this.constructor.name}.getToken`, options, async () => {
const tenantId = processMultiTenantRequest(this.tenantId, options, this.additionallyAllowedTenantIds);
const scope = typeof scopes === "string" ? scopes : scopes[0];
if (tenantId) {
checkTenantId(logger, tenantId);
}
try {
ensureValidScopeForDevTimeCreds(scope, logger);
logger.getToken.info(`Using the scope ${scope}`);
const resource = getScopeResource(scope);
const response = await this.getAzurePowerShellAccessToken(resource, tenantId, this.timeout);
logger.getToken.info(formatSuccess(scopes));
return {
token: response.Token,
expiresOnTimestamp: new Date(response.ExpiresOn).getTime(),
tokenType: "Bearer",
};
}
catch (err) {
if (isNotInstalledError(err)) {
const error = new CredentialUnavailableError(powerShellPublicErrorMessages.installed);
logger.getToken.info(formatError(scope, error));
throw error;
}
else if (isLoginError(err)) {
const error = new CredentialUnavailableError(powerShellPublicErrorMessages.login);
logger.getToken.info(formatError(scope, error));
throw error;
}
const error = new CredentialUnavailableError(`${err}. ${powerShellPublicErrorMessages.troubleshoot}`);
logger.getToken.info(formatError(scope, error));
throw error;
}
});
}
}
/**
*
* @internal
*/
export async function parseJsonToken(result) {
const jsonRegex = /{[^{}]*}/g;
const matches = result.match(jsonRegex);
let resultWithoutToken = result;
if (matches) {
try {
for (const item of matches) {
try {
const jsonContent = JSON.parse(item);
if (jsonContent?.Token) {
resultWithoutToken = resultWithoutToken.replace(item, "");
if (resultWithoutToken) {
logger.getToken.warning(resultWithoutToken);
}
return jsonContent;
}
}
catch (e) {
continue;
}
}
}
catch (e) {
throw new Error(`Unable to parse the output of PowerShell. Received output: ${result}`);
}
}
throw new Error(`No access token found in the output. Received output: ${result}`);
}
//# sourceMappingURL=azurePowerShellCredential.js.map