diff --git a/__tests__/PowerShell/AzPSLogin.test.ts b/__tests__/PowerShell/AzPSLogin.test.ts index 2beec4e39..0e57c0094 100644 --- a/__tests__/PowerShell/AzPSLogin.test.ts +++ b/__tests__/PowerShell/AzPSLogin.test.ts @@ -1,92 +1,92 @@ -import * as os from 'os'; - -import { AzPSLogin } from '../../src/PowerShell/AzPSLogin'; -import { LoginConfig } from '../../src/common/LoginConfig'; -import AzPSConstants from '../../src/PowerShell/AzPSConstants'; - -let azpsLogin: AzPSLogin; -jest.setTimeout(30000); - -beforeAll(() => { - var loginConfig = new LoginConfig(); - loginConfig.servicePrincipalId = "servicePrincipalID"; - loginConfig.servicePrincipalSecret = "servicePrincipalSecret"; - loginConfig.tenantId = "tenantId"; - loginConfig.subscriptionId = "subscriptionId"; - azpsLogin = new AzPSLogin(loginConfig); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('Testing login', () => { - let loginSpy; - - beforeEach(() => { - loginSpy = jest.spyOn(azpsLogin, 'login'); - }); - - test('ServicePrincipal login should pass', async () => { - loginSpy.mockImplementationOnce(() => Promise.resolve()); - await azpsLogin.login(); - expect(loginSpy).toHaveBeenCalled(); - }); -}); - -describe('Testing set module path', () => { - test('setDefaultPSModulePath should work', () => { - azpsLogin.setPSModulePathForGitHubRunner(); - const runner: string = process.env.RUNNER_OS || os.type(); - if(runner.toLowerCase() === "linux"){ - expect(process.env.PSModulePath).toContain(AzPSConstants.DEFAULT_AZ_PATH_ON_LINUX); - } - if(runner.toLowerCase().startsWith("windows")){ - expect(process.env.PSModulePath).toContain(AzPSConstants.DEFAULT_AZ_PATH_ON_WINDOWS); - } - }); - -}); - -describe('Testing runPSScript', () => { - test('Get PowerShell Version', async () => { - let script = `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - $output['Success'] = $true - $output['Result'] = $PSVersionTable.PSVersion.ToString() - } - catch { - $output['Success'] = $false - $output['Error'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - let psVersion: string = await AzPSLogin.runPSScript(script); - expect(psVersion === null).toBeFalsy(); - }); - - test('Get PowerShell Version with Wrong Name', async () => { - let script = `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - $output['Success'] = $true - $output['Result'] = $PSVersionTableWrongName.PSVersion.ToString() - } - catch { - $output['Success'] = $false - $output['Error'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - try{ - await AzPSLogin.runPSScript(script); - throw new Error("The last step should fail."); - }catch(error){ - expect(error.message.includes("Azure PowerShell login failed with error: You cannot call a method on a null-valued expression.")).toBeTruthy(); - } - }); - +import * as os from 'os'; + +import { AzPSLogin, setPSModulePathForGitHubRunner } from '../../src/PowerShell/AzPSLogin'; +import { LoginConfig } from '../../src/common/LoginConfig'; +import AzPSConstants from '../../src/PowerShell/AzPSConstants'; + +let azpsLogin: AzPSLogin; +jest.setTimeout(30000); + +beforeAll(() => { + var loginConfig = new LoginConfig(); + loginConfig.servicePrincipalId = "servicePrincipalID"; + loginConfig.servicePrincipalSecret = "servicePrincipalSecret"; + loginConfig.tenantId = "tenantId"; + loginConfig.subscriptionId = "subscriptionId"; + azpsLogin = new AzPSLogin(loginConfig); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('Testing login', () => { + let loginSpy; + + beforeEach(() => { + loginSpy = jest.spyOn(azpsLogin, 'login'); + }); + + test('ServicePrincipal login should pass', async () => { + loginSpy.mockImplementationOnce(() => Promise.resolve()); + await azpsLogin.login(); + expect(loginSpy).toHaveBeenCalled(); + }); +}); + +describe('Testing set module path', () => { + test('setDefaultPSModulePath should work', () => { + setPSModulePathForGitHubRunner(); + const runner: string = process.env.RUNNER_OS || os.type(); + if(runner.toLowerCase() === "linux"){ + expect(process.env.PSModulePath).toContain(AzPSConstants.DEFAULT_AZ_PATH_ON_LINUX); + } + if(runner.toLowerCase().startsWith("windows")){ + expect(process.env.PSModulePath).toContain(AzPSConstants.DEFAULT_AZ_PATH_ON_WINDOWS); + } + }); + +}); + +describe('Testing runPSScript', () => { + test('Get PowerShell Version', async () => { + let script = `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $output['Success'] = $true + $output['Result'] = $PSVersionTable.PSVersion.ToString() + } + catch { + $output['Success'] = $false + $output['Error'] = $_.exception.Message + } + return ConvertTo-Json $output`; + + let psVersion: string = await AzPSLogin.runPSScript(script); + expect(psVersion === null).toBeFalsy(); + }); + + test('Get PowerShell Version with Wrong Name', async () => { + let script = `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $output['Success'] = $true + $output['Result'] = $PSVersionTableWrongName.PSVersion.ToString() + } + catch { + $output['Success'] = $false + $output['Error'] = $_.exception.Message + } + return ConvertTo-Json $output`; + + try{ + await AzPSLogin.runPSScript(script); + throw new Error("The last step should fail."); + }catch(error){ + expect(error.message.includes("Azure PowerShell login failed with error: You cannot call a method on a null-valued expression.")).toBeTruthy(); + } + }); + }); \ No newline at end of file diff --git a/__tests__/PowerShell/AzPSScriptBuilder.test.ts b/__tests__/PowerShell/AzPSScriptBuilder.test.ts index 6a42060be..f835dd89c 100644 --- a/__tests__/PowerShell/AzPSScriptBuilder.test.ts +++ b/__tests__/PowerShell/AzPSScriptBuilder.test.ts @@ -1,153 +1,153 @@ -import AzPSSCriptBuilder from "../../src/PowerShell/AzPSScriptBuilder"; -import { LoginConfig } from "../../src/common/LoginConfig"; - -describe("Getting AzLogin PS script", () => { - - function setEnv(name: string, value: string) { - process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value; - } - - function cleanEnv() { - for (const envKey in process.env) { - if (envKey.startsWith('INPUT_')) { - delete process.env[envKey] - } - } - } - - beforeEach(() => { - cleanEnv(); - }); - - test('getImportLatestModuleScript', () => { - expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("(Get-Module -Name 'TestModule' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path"); - expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("Import-Module -Name $latestModulePath"); - }); - - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'true'); - setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { - 'clientId': 'client-id', - 'clientSecret': "client-secret", - 'tenantId': 'tenant-id', - 'subscriptionId': 'subscription-id' - } - setEnv('creds', JSON.stringify(creds)); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; $psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('service principal with secret'); - }); - }); - - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true, secret with single-quote', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'true'); - setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { - 'clientId': 'client-id', - 'clientSecret': "client-se'cret", - 'tenantId': 'tenant-id', - 'subscriptionId': 'subscription-id' - } - setEnv('creds', JSON.stringify(creds)); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; $psLoginSecrets = ConvertTo-SecureString 'client-se''cret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('service principal with secret'); - }); - }); - - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=false', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); // same as true - setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { - 'clientId': 'client-id', - 'clientSecret': 'client-secret', - 'tenantId': 'tenant-id', - 'subscriptionId': 'subscription-id' - } - setEnv('creds', JSON.stringify(creds)); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; $psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('service principal with secret'); - }); - }); - - test('getAzPSLoginScript for OIDC', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - setEnv('tenant-id', 'tenant-id'); - setEnv('subscription-id', 'subscription-id'); - setEnv('client-id', 'client-id'); - setEnv('auth-type', 'SERVICE_PRINCIPAL'); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => {loginConfig.federatedToken = "fake-token";}); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -ApplicationId 'client-id' -FederatedToken 'fake-token' | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('OIDC'); - }); - }); - - test('getAzPSLoginScript for System MI', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - setEnv('subscription-id', 'subscription-id'); - setEnv('auth-type', 'IDENTITY'); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; Connect-AzAccount -Identity -Environment 'azurecloud' -Subscription 'subscription-id' | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('system-assigned managed identity'); - }); - }); - - test('getAzPSLoginScript for System MI without subscription id', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - // setEnv('subscription-id', 'subscription-id'); - setEnv('auth-type', 'IDENTITY'); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; Connect-AzAccount -Identity -Environment 'azurecloud' | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('system-assigned managed identity'); - }); - }); - - test('getAzPSLoginScript for user-assigned MI', () => { - setEnv('environment', 'azurecloud'); - setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'true'); - setEnv('auth-type', 'IDENTITY'); - setEnv('client-id', 'client-id'); - - let loginConfig = new LoginConfig(); - loginConfig.initialize(); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Clear-AzContext -Scope Process; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; Connect-AzAccount -Identity -Environment 'azurecloud' -AccountId 'client-id' | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('user-assigned managed identity'); - }); - }); - +import AzPSSCriptBuilder from "../../src/PowerShell/AzPSScriptBuilder"; +import { LoginConfig } from "../../src/common/LoginConfig"; + +describe("Getting AzLogin PS script", () => { + + function setEnv(name: string, value: string) { + process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value; + } + + function cleanEnv() { + for (const envKey in process.env) { + if (envKey.startsWith('INPUT_')) { + delete process.env[envKey] + } + } + } + + beforeEach(() => { + cleanEnv(); + }); + + test('getImportLatestModuleScript', () => { + expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("(Get-Module -Name 'TestModule' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path"); + expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("Import-Module -Name $latestModulePath"); + }); + + test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + let creds = { + 'clientId': 'client-id', + 'clientSecret': "client-secret", + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + setEnv('creds', JSON.stringify(creds)); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('service principal with secret'); + }); + }); + + test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true, secret with single-quote', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + let creds = { + 'clientId': 'client-id', + 'clientSecret': "client-se'cret", + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + setEnv('creds', JSON.stringify(creds)); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-se''cret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('service principal with secret'); + }); + }); + + test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=false', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); // same as true + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + let creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + setEnv('creds', JSON.stringify(creds)); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('service principal with secret'); + }); + }); + + test('getAzPSLoginScript for OIDC', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => {loginConfig.federatedToken = "fake-token";}); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -ApplicationId 'client-id' -FederatedToken 'fake-token' | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('OIDC'); + }); + }); + + test('getAzPSLoginScript for System MI', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('subscription-id', 'subscription-id'); + setEnv('auth-type', 'IDENTITY'); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -Subscription 'subscription-id' | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('system-assigned managed identity'); + }); + }); + + test('getAzPSLoginScript for System MI without subscription id', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + // setEnv('subscription-id', 'subscription-id'); + setEnv('auth-type', 'IDENTITY'); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('system-assigned managed identity'); + }); + }); + + test('getAzPSLoginScript for user-assigned MI', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'IDENTITY'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { + expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -AccountId 'client-id' | out-null;")).toBeTruthy(); + expect(loginMethod).toBe('user-assigned managed identity'); + }); + }); + }); \ No newline at end of file diff --git a/src/PowerShell/AzPSLogin.ts b/src/PowerShell/AzPSLogin.ts index c14f327e7..84d60a7bd 100644 --- a/src/PowerShell/AzPSLogin.ts +++ b/src/PowerShell/AzPSLogin.ts @@ -1,100 +1,98 @@ -import * as core from '@actions/core'; -import * as exec from '@actions/exec'; -import * as io from '@actions/io'; -import * as os from 'os'; -import * as path from 'path'; - -import AzPSScriptBuilder from './AzPSScriptBuilder'; -import AzPSConstants from './AzPSConstants'; -import { LoginConfig } from '../common/LoginConfig'; - -interface PSResultType { - Result: string; - Success: boolean; - Error: string; -} - -export class AzPSLogin { - loginConfig: LoginConfig; - - constructor(loginConfig: LoginConfig) { - this.loginConfig = loginConfig; - } - - async login() { - core.info(`Running Azure PowerShell Login.`); - this.setPSModulePathForGitHubRunner(); - await this.importLatestAzAccounts(); - - const [loginMethod, loginScript] = await AzPSScriptBuilder.getAzPSLoginScript(this.loginConfig); - core.info(`Attempting Azure PowerShell login by using ${loginMethod}...`); - core.debug(`Azure PowerShell Login Script: ${loginScript}`); - await AzPSLogin.runPSScript(loginScript); - console.log(`Running Azure PowerShell Login successfully.`); - } - - setPSModulePathForGitHubRunner() { - const runner: string = process.env.RUNNER_OS || os.type(); - switch (runner.toLowerCase()) { - case "linux": - this.pushPSModulePath(AzPSConstants.DEFAULT_AZ_PATH_ON_LINUX); - break; - case "windows": - case "windows_nt": - this.pushPSModulePath(AzPSConstants.DEFAULT_AZ_PATH_ON_WINDOWS); - break; - case "macos": - case "darwin": - core.warning(`Skip setting the default PowerShell module path for OS ${runner.toLowerCase()}.`); - break; - default: - core.warning(`Skip setting the default PowerShell module path for unknown OS ${runner.toLowerCase()}.`); - break; - } - } - - private pushPSModulePath(psModulePath: string) { - process.env.PSModulePath = `${psModulePath}${path.delimiter}${process.env.PSModulePath}`; - core.debug(`Set PSModulePath as ${process.env.PSModulePath}`); - } - - private async importLatestAzAccounts() { - let importLatestAccountsScript: string = AzPSScriptBuilder.getImportLatestModuleScript(AzPSConstants.AzAccounts); - core.debug(`The script to import the latest Az.Accounts: ${importLatestAccountsScript}`); - let azAccountsPath: string = await AzPSLogin.runPSScript(importLatestAccountsScript); - core.debug(`The latest Az.Accounts used: ${azAccountsPath}`); - } - - static async runPSScript(psScript: string): Promise { - let outputString: string = ""; - let commandStdErr = false; - const options: any = { - silent: true, - listeners: { - stdout: (data: Buffer) => { - outputString += data.toString(); - }, - stderr: (data: Buffer) => { - let error = data.toString(); - if (error && error.trim().length !== 0) { - commandStdErr = true; - core.error(error); - } - } - } - }; - - let psPath: string = await io.which(AzPSConstants.PowerShell_CmdName, true); - await exec.exec(`"${psPath}"`, ["-Command", psScript], options) - if (commandStdErr) { - throw new Error('Azure PowerShell login failed with errors.'); - } - const result: PSResultType = JSON.parse(outputString.trim()); - console.log(result); - if (!(result.Success)) { - throw new Error(`Azure PowerShell login failed with error: ${result.Error}`); - } - return result.Result; - } -} - +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import * as os from 'os'; +import * as path from 'path'; + +import AzPSScriptBuilder from './AzPSScriptBuilder'; +import AzPSConstants from './AzPSConstants'; +import { LoginConfig } from '../common/LoginConfig'; + +interface PSResultType { + Result: string; + Success: boolean; + Error: string; +} + +export class AzPSLogin { + loginConfig: LoginConfig; + + constructor(loginConfig: LoginConfig) { + this.loginConfig = loginConfig; + } + + async login() { + core.info(`Running Azure PowerShell Login.`); + setPSModulePathForGitHubRunner(); + await importLatestAzAccounts(); + const [loginMethod, loginScript] = await AzPSScriptBuilder.getAzPSLoginScript(this.loginConfig); + core.info(`Attempting Azure PowerShell login by using ${loginMethod}...`); + core.debug(`Azure PowerShell Login Script: ${loginScript}`); + await AzPSLogin.runPSScript(loginScript); + console.log(`Running Azure PowerShell Login successfully.`); + } + + static async runPSScript(psScript: string): Promise { + let outputString: string = ""; + let commandStdErr = false; + const options: any = { + silent: true, + listeners: { + stdout: (data: Buffer) => { + outputString += data.toString(); + }, + stderr: (data: Buffer) => { + let error = data.toString(); + if (error && error.trim().length !== 0) { + commandStdErr = true; + core.error(error); + } + } + } + }; + + let psPath: string = await io.which(AzPSConstants.PowerShell_CmdName, true); + await exec.exec(`"${psPath}"`, ["-Command", psScript], options) + if (commandStdErr) { + throw new Error('Azure PowerShell login failed with errors.'); + } + const result: PSResultType = JSON.parse(outputString.trim()); + console.log(result); + if (!(result.Success)) { + throw new Error(`Azure PowerShell login failed with error: ${result.Error}`); + } + return result.Result; + } +} + +export function setPSModulePathForGitHubRunner() { + const runner: string = process.env.RUNNER_OS || os.type(); + switch (runner.toLowerCase()) { + case "linux": + pushPSModulePath(AzPSConstants.DEFAULT_AZ_PATH_ON_LINUX); + break; + case "windows": + case "windows_nt": + pushPSModulePath(AzPSConstants.DEFAULT_AZ_PATH_ON_WINDOWS); + break; + case "macos": + case "darwin": + core.warning(`Skip setting the default PowerShell module path for OS ${runner.toLowerCase()}.`); + break; + default: + core.warning(`Skip setting the default PowerShell module path for unknown OS ${runner.toLowerCase()}.`); + break; + } +} + +async function pushPSModulePath(psModulePath: string) { + process.env.PSModulePath = `${psModulePath}${path.delimiter}${process.env.PSModulePath}`; + core.debug(`Set PSModulePath as ${process.env.PSModulePath}`); +} + +export async function importLatestAzAccounts() { + let importLatestAccountsScript: string = AzPSScriptBuilder.getImportLatestModuleScript(AzPSConstants.AzAccounts); + core.debug(`The script to import the latest Az.Accounts: ${importLatestAccountsScript}`); + let azAccountsPath: string = await AzPSLogin.runPSScript(importLatestAccountsScript); + core.debug(`The latest Az.Accounts used: ${azAccountsPath}`); +} diff --git a/src/PowerShell/AzPSScriptBuilder.ts b/src/PowerShell/AzPSScriptBuilder.ts index 81112f3d8..619312eaa 100644 --- a/src/PowerShell/AzPSScriptBuilder.ts +++ b/src/PowerShell/AzPSScriptBuilder.ts @@ -1,113 +1,112 @@ -import AzPSConstants from "./AzPSConstants"; -import { LoginConfig } from '../common/LoginConfig'; - -export default class AzPSScriptBuilder { - - static getImportLatestModuleScript(moduleName: string): string { - let script = `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - $latestModulePath = (Get-Module -Name '${moduleName}' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path - Import-Module -Name $latestModulePath - $output['Success'] = $true - $output['Result'] = $latestModulePath - } - catch { - $output['Success'] = $false - $output['Error'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - return script; - } - - static async getAzPSLoginScript(loginConfig: LoginConfig) { - let loginMethodName = ""; - let commands = 'Clear-AzContext -Scope Process; '; - commands += 'Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue; '; - - if (loginConfig.environment.toLowerCase() == "azurestack") { - commands += `Add-AzEnvironment -Name '${loginConfig.environment}' -ARMEndpoint '${loginConfig.resourceManagerEndpointUrl}' | out-null;`; - } - if (loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { - if (loginConfig.servicePrincipalSecret) { - commands += AzPSScriptBuilder.loginWithSecret(loginConfig); - loginMethodName = 'service principal with secret'; - } else { - commands += await AzPSScriptBuilder.loginWithOIDC(loginConfig); - loginMethodName = "OIDC"; - } - } else { - if (loginConfig.servicePrincipalId) { - commands += AzPSScriptBuilder.loginWithUserAssignedIdentity(loginConfig); - loginMethodName = 'user-assigned managed identity'; - } else { - commands += AzPSScriptBuilder.loginWithSystemAssignedIdentity(loginConfig); - loginMethodName = 'system-assigned managed identity'; - } - } - - let script = `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - ${commands} - $output['Success'] = $true - $output['Result'] = "" - } - catch { - $output['Success'] = $false - $output['Error'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - return [loginMethodName, script]; - } - - private static loginWithSecret(loginConfig: LoginConfig): string { - let servicePrincipalSecret: string = loginConfig.servicePrincipalSecret.split("'").join("''"); - let loginCmdlet = `$psLoginSecrets = ConvertTo-SecureString '${servicePrincipalSecret}' -AsPlainText -Force; `; - loginCmdlet += `$psLoginCredential = New-Object System.Management.Automation.PSCredential('${loginConfig.servicePrincipalId}', $psLoginSecrets); `; - - let cmdletSuffix = "-Credential $psLoginCredential"; - loginCmdlet += AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - - return loginCmdlet; - } - - private static async loginWithOIDC(loginConfig: LoginConfig) { - await loginConfig.getFederatedToken(); - let cmdletSuffix = `-ApplicationId '${loginConfig.servicePrincipalId}' -FederatedToken '${loginConfig.federatedToken}'`; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - private static loginWithSystemAssignedIdentity(loginConfig: LoginConfig): string { - let cmdletSuffix = ""; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - static loginWithUserAssignedIdentity(loginConfig: LoginConfig): string { - let cmdletSuffix = `-AccountId '${loginConfig.servicePrincipalId}'`; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - private static psLoginCmdlet(authType:string, environment:string, tenantId:string, subscriptionId:string, cmdletSuffix:string){ - let loginCmdlet = `Connect-AzAccount `; - if(authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL){ - loginCmdlet += "-ServicePrincipal "; - }else{ - loginCmdlet += "-Identity "; - } - loginCmdlet += `-Environment '${environment}' `; - if(tenantId){ - loginCmdlet += `-Tenant '${tenantId}' `; - } - if(subscriptionId){ - loginCmdlet += `-Subscription '${subscriptionId}' `; - } - loginCmdlet += `${cmdletSuffix} | out-null;`; - return loginCmdlet; - } -} - +import AzPSConstants from "./AzPSConstants"; +import { LoginConfig } from '../common/LoginConfig'; + +export default class AzPSScriptBuilder { + + static getImportLatestModuleScript(moduleName: string): string { + let script = `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + $latestModulePath = (Get-Module -Name '${moduleName}' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path + Import-Module -Name $latestModulePath + $output['Success'] = $true + $output['Result'] = $latestModulePath + } + catch { + $output['Success'] = $false + $output['Error'] = $_.exception.Message + } + return ConvertTo-Json $output`; + + return script; + } + + static async getAzPSLoginScript(loginConfig: LoginConfig) { + let loginMethodName = ""; + let commands = ""; + + if (loginConfig.environment.toLowerCase() == "azurestack") { + commands += `Add-AzEnvironment -Name '${loginConfig.environment}' -ARMEndpoint '${loginConfig.resourceManagerEndpointUrl}' | out-null;`; + } + if (loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { + if (loginConfig.servicePrincipalSecret) { + commands += AzPSScriptBuilder.loginWithSecret(loginConfig); + loginMethodName = 'service principal with secret'; + } else { + commands += await AzPSScriptBuilder.loginWithOIDC(loginConfig); + loginMethodName = "OIDC"; + } + } else { + if (loginConfig.servicePrincipalId) { + commands += AzPSScriptBuilder.loginWithUserAssignedIdentity(loginConfig); + loginMethodName = 'user-assigned managed identity'; + } else { + commands += AzPSScriptBuilder.loginWithSystemAssignedIdentity(loginConfig); + loginMethodName = 'system-assigned managed identity'; + } + } + + let script = `try { + $ErrorActionPreference = "Stop" + $WarningPreference = "SilentlyContinue" + $output = @{} + ${commands} + $output['Success'] = $true + $output['Result'] = "" + } + catch { + $output['Success'] = $false + $output['Error'] = $_.exception.Message + } + return ConvertTo-Json $output`; + + return [loginMethodName, script]; + } + + private static loginWithSecret(loginConfig: LoginConfig): string { + let servicePrincipalSecret: string = loginConfig.servicePrincipalSecret.split("'").join("''"); + let loginCmdlet = `$psLoginSecrets = ConvertTo-SecureString '${servicePrincipalSecret}' -AsPlainText -Force; `; + loginCmdlet += `$psLoginCredential = New-Object System.Management.Automation.PSCredential('${loginConfig.servicePrincipalId}', $psLoginSecrets); `; + + let cmdletSuffix = "-Credential $psLoginCredential"; + loginCmdlet += AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); + + return loginCmdlet; + } + + private static async loginWithOIDC(loginConfig: LoginConfig) { + await loginConfig.getFederatedToken(); + let cmdletSuffix = `-ApplicationId '${loginConfig.servicePrincipalId}' -FederatedToken '${loginConfig.federatedToken}'`; + return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); + } + + private static loginWithSystemAssignedIdentity(loginConfig: LoginConfig): string { + let cmdletSuffix = ""; + return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); + } + + static loginWithUserAssignedIdentity(loginConfig: LoginConfig): string { + let cmdletSuffix = `-AccountId '${loginConfig.servicePrincipalId}'`; + return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); + } + + private static psLoginCmdlet(authType:string, environment:string, tenantId:string, subscriptionId:string, cmdletSuffix:string){ + let loginCmdlet = `Connect-AzAccount `; + if(authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL){ + loginCmdlet += "-ServicePrincipal "; + }else{ + loginCmdlet += "-Identity "; + } + loginCmdlet += `-Environment '${environment}' `; + if(tenantId){ + loginCmdlet += `-Tenant '${tenantId}' `; + } + if(subscriptionId){ + loginCmdlet += `-Subscription '${subscriptionId}' `; + } + loginCmdlet += `${cmdletSuffix} | out-null;`; + return loginCmdlet; + } +} + diff --git a/src/cleanup.ts b/src/cleanup.ts index 45520d2e8..bba17e1d4 100644 --- a/src/cleanup.ts +++ b/src/cleanup.ts @@ -1,12 +1,22 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as io from '@actions/io'; +import { setPSModulePathForGitHubRunner, importLatestAzAccounts } from './PowerShell/AzPSLogin'; +import AzPSConstants from './PowerShell/AzPSConstants'; async function cleanup() { try { let azPath = await io.which("az", true); - core.info("Clearing accounts from the local cache.") + core.info("Clearing azure cli accounts from the local cache.") await exec.exec(`"${azPath}"`, ["account", "clear"]); + + let psPath = await io.which(AzPSConstants.PowerShell_CmdName, true); + core.debug("Importing Azure PowerShell module."); + setPSModulePathForGitHubRunner(); + await importLatestAzAccounts(); + core.info("Clearing azure powershell accounts from the local cache."); + await exec.exec(`"${psPath}"`, ["-Command", "Clear-AzContext", "-Scope", "Process"]); + await exec.exec(`"${psPath}"`, ["-Command", "Clear-AzContext", "-Scope", "CurrentUser", "-Force", "-ErrorAction", "SilentlyContinue"]); } catch (error) { core.setFailed(`Login cleanup failed with ${error}.`); @@ -14,4 +24,5 @@ async function cleanup() { } } -cleanup(); \ No newline at end of file +cleanup(); + diff --git a/src/main.ts b/src/main.ts index b84b803c3..adb94e0d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,7 @@ async function main() { } } catch (error) { - core.setFailed(`Login failed with ${error}. If 'enable-AzPSSession' is true, make sure 'pwsh' is installed on the runner together with Azure PowerShell module. Double check if the 'auth-type' is correct. Refer to https://github.com/Azure/login#readme for more information.`); + core.setFailed(`Login failed with ${error}. Double check if the 'auth-type' is correct. Refer to https://github.com/Azure/login#readme for more information.`); core.debug(error.stack); } finally { diff --git a/src/setup.ts b/src/setup.ts index da8070ea1..20ee7b1b3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,6 +1,9 @@ import * as core from '@actions/core'; import * as exec from '@actions/exec'; import * as io from '@actions/io'; +import { setPSModulePathForGitHubRunner, importLatestAzAccounts } from './PowerShell/AzPSLogin'; +import AzPSConstants from './PowerShell/AzPSConstants'; + async function setup() { try { @@ -9,14 +12,26 @@ async function setup() { throw new Error("Azure CLI is not found in the runner."); } core.debug(`Azure CLI path: ${azPath}`); - - core.info("Clearing accounts from the local cache.") + core.info("Clearing azure cli accounts from the local cache."); await exec.exec(`"${azPath}"`, ["account", "clear"]); + + let psPath: string = await io.which(AzPSConstants.PowerShell_CmdName, true); + if (!psPath) { + throw new Error("PowerShell is not found in the runner."); + } + core.debug(`PowerShell path: ${psPath}`); + core.debug("Importing Azure PowerShell module."); + setPSModulePathForGitHubRunner(); + await importLatestAzAccounts(); + core.info("Clearing azure powershell accounts from the local cache."); + await exec.exec(`"${psPath}"`, ["-Command", "Clear-AzContext", "-Scope", "Process"]); + await exec.exec(`"${psPath}"`, ["-Command", "Clear-AzContext", "-Scope", "CurrentUser", "-Force", "-ErrorAction", "SilentlyContinue"]); } catch (error) { - core.setFailed(`Login setup failed with ${error}. Make sure 'az' is installed on the runner.`); + core.setFailed(`Login setup failed with ${error}. Make sure 'az' is installed on the runner. If 'enable-AzPSSession' is true, make sure 'pwsh' is installed on the runner together with Azure PowerShell module.`); core.debug(error.stack); } } -setup(); \ No newline at end of file +setup(); +