diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml index d6eeb0d7..0d8a4b39 100644 --- a/.github/workflows/azure-login-negative.yml +++ b/.github/workflows/azure-login-negative.yml @@ -26,35 +26,15 @@ jobs: run: | npm install npm run build - - - name: 'Run L0 tests' - id: run_test - continue-on-error: true - run: | npm run test - - - name: Check Last step failed - if: steps.run_test.outcome == 'success' - uses: actions/github-script@v3 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - name: Login with creds - id: login_1 continue-on-error: true uses: ./ with: creds: ${{secrets.SP1}} enable-AzPSSession: true - - name: Check Last step failed - if: steps.login_1.outcome == 'success' - uses: actions/github-script@v3 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - name: Run Azure Cli run: | az account show @@ -81,22 +61,14 @@ jobs: - name: Login with individual parameters id: login_2 - continue-on-error: true uses: ./ with: client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + # subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true enable-AzPSSession: true - - name: Check Last step failed - if: steps.login_2.outcome == 'success' - uses: actions/github-script@v3 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - name: Run Azure Cli again run: | az account show @@ -144,7 +116,7 @@ jobs: with: client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + # subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true enable-AzPSSession: true @@ -356,4 +328,90 @@ jobs: uses: actions/github-script@v3 with: script: | - core.setFailed('Last action should fail but not. Please check it.') \ No newline at end of file + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with tenant-level account, without allow-no-subscriptions + id: login_11 + continue-on-error: true + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_11.outcome == 'success' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + # Secret of SP1 in creds will be used to sign in SP2 + - name: Login with both creds and individual parameters + id: login_12 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP1}} + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_12.outcome == 'success' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login by OIDC with all info in creds + id: login_13 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP2}} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_13.outcome == 'success' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + VMTest: + strategy: + matrix: + os: [self_linux, self_windows] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + - name: 'Checking out repo code' + uses: actions/checkout@v3.5.2 + + - name: Set Node.js 16.x for GitHub Action + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: Login with system-assigned managed identity without auth-type + id: login_14 + continue-on-error: true + uses: ./ + + - name: Check Last step failed + if: steps.login_14.outcome == 'success' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml index 49a3cf92..71a80127 100644 --- a/.github/workflows/azure-login-positive.yml +++ b/.github/workflows/azure-login-positive.yml @@ -60,7 +60,7 @@ jobs: with: client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + # subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true enable-AzPSSession: true @@ -75,6 +75,28 @@ jobs: inlineScript: | Get-AzContext | Format-List + - name: Login with explicit auth-type + uses: ./ + with: + creds: ${{secrets.SP1}} + auth-type: SERVICE_PRINCIPAL + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show + az group show --name GitHubAction_CI_RG + az vm list + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + Get-AzResourceGroup -Name GitHubAction_CI_RG + Get-AzVM + ParameterTest: strategy: matrix: @@ -96,30 +118,6 @@ jobs: npm install npm run build - - name: Login with both creds and individual parameters - uses: ./ - with: - creds: ${{secrets.SP1}} - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - - name: Run Azure Cli - run: | - az account show - az group show --name GitHubAction_CI_RG - az vm list - - - name: Run Azure PowerShell - uses: azure/powershell@v1.2.0 - with: - azPSVersion: "latest" - inlineScript: | - Get-AzContext | Format-List - Get-AzResourceGroup -Name GitHubAction_CI_RG - Get-AzVM - - name: Login with creds, disable ps session uses: ./ with: @@ -185,3 +183,107 @@ jobs: inlineScript: | Get-AzContext | Format-List + VMTest: + strategy: + matrix: + os: [self_linux, self_windows] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + - name: 'Checking out repo code' + uses: actions/checkout@v3.5.2 + + - name: Set Node.js 16.x for GitHub Action + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: Login with system-assigned managed identity, no subscription-id + uses: ./ + with: + auth-type: IDENTITY + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + + - name: Login with system-assigned managed identity, with subscription id + uses: ./ + with: + auth-type: IDENTITY + subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }} + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show + az group show --name GitHubAction_CI_RG + az vm list + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + Get-AzResourceGroup -Name GitHubAction_CI_RG + Get-AzVM + + - name: Login with tenant-level user-assigned managed identity with allow-no-subscriptions + uses: ./ + with: + client-id: ${{ secrets.UMI2_CLIENT_ID }} + allow-no-subscriptions: true + auth-type: IDENTITY + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + + - name: Login with user-assigned managed identity, subscription-id + uses: ./ + with: + client-id: ${{ secrets.UMI1_CLIENT_ID }} + subscription-id: ${{ secrets.UMI1_SUBSCRIPTION_ID }} + auth-type: IDENTITY + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show + az group show --name GitHubAction_CI_RG + az vm list + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + Get-AzResourceGroup -Name GitHubAction_CI_RG + Get-AzVM + + diff --git a/__tests__/LoginConfig.test.ts b/__tests__/LoginConfig.test.ts new file mode 100644 index 00000000..855af818 --- /dev/null +++ b/__tests__/LoginConfig.test.ts @@ -0,0 +1,259 @@ +import { LoginConfig } from "../src/common/LoginConfig"; + +describe("LoginConfig Test", () => { + + 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] + } + } + } + + async function testCreds(creds:any){ + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('creds', JSON.stringify(creds)); + let loginConfig = new LoginConfig(); + try{ + await loginConfig.initialize(); + throw new Error("The last step should fail."); + }catch(error){ + expect(error.message.includes("Not all parameters are provided in 'creds'.")).toBeTruthy(); + } + } + + function testValidateWithErrorMessage(loginConfig:LoginConfig, errorMessage:string){ + try{ + loginConfig.validate(); + throw new Error("The last step should fail."); + }catch(error){ + expect(error.message.includes(errorMessage)).toBeTruthy(); + } + } + + beforeEach(() => { + cleanEnv(); + }); + + test('initialize with creds, lack of clientId', async () => { + let creds1 = { + // 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + await testCreds(creds1); + + }); + + test('initialize with creds, lack of clientSecret', async () => { + let creds1 = { + 'clientId': 'client-id', + // 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + await testCreds(creds1); + + }); + + test('initialize with creds, lack of tenantId', async () => { + let creds1 = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + // 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + await testCreds(creds1); + + }); + + test('initialize with creds, lack of subscriptionId', async () => { + let creds1 = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + // 'subscriptionId': 'subscription-id' + } + await testCreds(creds1); + + }); + + test('initialize with creds', async () => { + let creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + } + + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('creds', JSON.stringify(creds)); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.environment).toBe("azurecloud"); + expect(loginConfig.enableAzPSSession).toBeTruthy(); + expect(loginConfig.allowNoSubscriptionsLogin).toBeFalsy(); + expect(loginConfig.authType).toBe("SERVICE_PRINCIPAL"); + expect(loginConfig.servicePrincipalId).toBe("client-id"); + expect(loginConfig.servicePrincipalSecret).toBe("client-secret"); + expect(loginConfig.tenantId).toBe("tenant-id"); + expect(loginConfig.subscriptionId).toBe("subscription-id"); + }); + + test('initialize with individual parameters', async () => { + setEnv('environment', 'azureusgovernment'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.environment).toBe("azureusgovernment"); + expect(loginConfig.enableAzPSSession).toBeFalsy(); + expect(loginConfig.allowNoSubscriptionsLogin).toBeTruthy(); + expect(loginConfig.authType).toBe("SERVICE_PRINCIPAL"); + expect(loginConfig.servicePrincipalId).toBe("client-id"); + expect(loginConfig.tenantId).toBe("tenant-id"); + expect(loginConfig.subscriptionId).toBe("subscription-id"); + }); + + test('initialize with both creds and individual parameters', async () => { + setEnv('environment', 'azureusgovernment'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + + setEnv('tenant-id', 'tenant-id-aa'); + setEnv('subscription-id', 'subscription-id-aa'); + setEnv('client-id', 'client-id-aa'); + + let creds = { + 'clientId': 'client-id-bb', + 'clientSecret': 'client-secret-bb', + 'tenantId': 'tenant-id-bb', + 'subscriptionId': 'subscription-id-bb' + } + setEnv('creds', JSON.stringify(creds)); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + expect(loginConfig.environment).toBe("azureusgovernment"); + expect(loginConfig.enableAzPSSession).toBeFalsy(); + expect(loginConfig.allowNoSubscriptionsLogin).toBeTruthy(); + expect(loginConfig.authType).toBe("SERVICE_PRINCIPAL"); + expect(loginConfig.servicePrincipalId).toBe("client-id-aa"); + expect(loginConfig.servicePrincipalSecret).toBeNull(); + expect(loginConfig.tenantId).toBe("tenant-id-aa"); + expect(loginConfig.subscriptionId).toBe("subscription-id-aa"); + }); + + test('validate with wrong environment', async () => { + setEnv('environment', 'aWrongCloud'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + testValidateWithErrorMessage(loginConfig, "Unsupported value 'awrongcloud' for environment is passed."); + }); + + test('validate with wrong authType', async () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE-PRINCIPAL'); + + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + testValidateWithErrorMessage(loginConfig, "Unsupported value 'SERVICE-PRINCIPAL' for authentication type is passed."); + }); + + test('validate with SERVICE_PRINCIPAL, lack of tenant id', async () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + + // setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + testValidateWithErrorMessage(loginConfig, "Using auth-type: SERVICE_PRINCIPAL. Not all values are present. Ensure 'client-id' and 'tenant-id' are supplied."); + }); + + test('validate with SERVICE_PRINCIPAL, lack of client id', async () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + // setEnv('client-id', 'client-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + testValidateWithErrorMessage(loginConfig, "Using auth-type: SERVICE_PRINCIPAL. Not all values are present. Ensure 'client-id' and 'tenant-id' are supplied."); + }); + + test('validate without subscriptionId and allowNoSubscriptionsLogin=false', async () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'false'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('auth-type', 'IDENTITY'); + + // setEnv('subscription-id', 'subscription-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + testValidateWithErrorMessage(loginConfig, "Ensure subscriptionId is supplied."); + }); + + test('validate without subscriptionId and allowNoSubscriptionsLogin=true', async () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'IDENTITY'); + + // setEnv('subscription-id', 'subscription-id'); + + let loginConfig = new LoginConfig(); + await loginConfig.initialize(); + loginConfig.validate(); + expect(loginConfig.environment).toBe("azurestack"); + expect(loginConfig.enableAzPSSession).toBeTruthy(); + expect(loginConfig.allowNoSubscriptionsLogin).toBeTruthy(); + expect(loginConfig.authType).toBe("IDENTITY"); + expect(loginConfig.servicePrincipalId).toBe(""); + expect(loginConfig.servicePrincipalSecret).toBeNull(); + expect(loginConfig.tenantId).toBe(""); + expect(loginConfig.subscriptionId).toBe(""); + }); + +}); \ No newline at end of file diff --git a/__tests__/PowerShell/AzPSLogin.test.ts b/__tests__/PowerShell/AzPSLogin.test.ts new file mode 100644 index 00000000..2beec4e3 --- /dev/null +++ b/__tests__/PowerShell/AzPSLogin.test.ts @@ -0,0 +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(); + } + }); + +}); \ No newline at end of file diff --git a/__tests__/PowerShell/AzPSScriptBuilder.test.ts b/__tests__/PowerShell/AzPSScriptBuilder.test.ts new file mode 100644 index 00000000..6a42060b --- /dev/null +++ b/__tests__/PowerShell/AzPSScriptBuilder.test.ts @@ -0,0 +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'); + }); + }); + +}); \ No newline at end of file diff --git a/__tests__/PowerShell/ServicePrinicipalLogin.test.ts b/__tests__/PowerShell/ServicePrinicipalLogin.test.ts deleted file mode 100644 index 95a93950..00000000 --- a/__tests__/PowerShell/ServicePrinicipalLogin.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ServicePrincipalLogin } from '../../src/PowerShell/ServicePrincipalLogin'; -import { LoginConfig } from '../../src/common/LoginConfig'; - -jest.mock('../../src/PowerShell/Utilities/Utils'); -jest.mock('../../src/PowerShell/Utilities/PowerShellToolRunner'); -let spnlogin: ServicePrincipalLogin; - -beforeAll(() => { - var loginConfig = new LoginConfig(); - loginConfig.servicePrincipalId = "servicePrincipalID"; - loginConfig.servicePrincipalKey = "servicePrinicipalkey"; - loginConfig.tenantId = "tenantId"; - loginConfig.subscriptionId = "subscriptionId"; - spnlogin = new ServicePrincipalLogin(loginConfig); -}); - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('Testing initialize', () => { - let initializeSpy; - - beforeEach(() => { - initializeSpy = jest.spyOn(spnlogin, 'initialize'); - }); - test('ServicePrincipalLogin initialize should pass', async () => { - await spnlogin.initialize(); - expect(initializeSpy).toHaveBeenCalled(); - }); -}); - -describe('Testing login', () => { - let loginSpy; - - beforeEach(() => { - loginSpy = jest.spyOn(spnlogin, 'login'); - }); - test('ServicePrincipal login should pass', async () => { - loginSpy.mockImplementationOnce(() => Promise.resolve()); - await spnlogin.login(); - expect(loginSpy).toHaveBeenCalled(); - }); -}); diff --git a/__tests__/PowerShell/Utilities/ScriptBuilder.test.ts b/__tests__/PowerShell/Utilities/ScriptBuilder.test.ts deleted file mode 100644 index 291cbd04..00000000 --- a/__tests__/PowerShell/Utilities/ScriptBuilder.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ScriptBuilder from "../../../src/PowerShell/Utilities/ScriptBuilder"; -import Constants from "../../../src/PowerShell/Constants"; - -describe("Getting AzLogin PS script" , () => { - const scheme = Constants.ServicePrincipal; - let args: any = { - servicePrincipalId: "service-principal-id", - servicePrincipalKey: "service-principal-key", - environment: "environment", - scopeLevel: Constants.Subscription, - subscriptionId: "subId", - allowNoSubscriptionsLogin: true - } - - test("PS script should not set context while passing allowNoSubscriptionsLogin as true", () => { - const loginScript = new ScriptBuilder().getAzPSLoginScript(scheme, "tenant-id", args); - expect(loginScript.includes("Set-AzContext -SubscriptionId")).toBeFalsy(); - }); - - test("PS script should set context while passing allowNoSubscriptionsLogin as false", () => { - args["allowNoSubscriptionsLogin"] = false; - const loginScript = new ScriptBuilder().getAzPSLoginScript(scheme, "tenant-id", args); - expect(loginScript.includes("Set-AzContext -SubscriptionId")).toBeTruthy(); - }); -}); \ No newline at end of file diff --git a/__tests__/PowerShell/Utilities/Utils.test.ts b/__tests__/PowerShell/Utilities/Utils.test.ts deleted file mode 100644 index f6e2825b..00000000 --- a/__tests__/PowerShell/Utilities/Utils.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Utils from '../../../src/PowerShell/Utilities/Utils'; - -const version: string = '9.0.0'; -const moduleName: string = 'az'; - -afterEach(() => { - jest.restoreAllMocks(); -}); - -describe('Testing isValidVersion', () => { - const validVersion: string = '1.2.4'; - const invalidVersion: string = 'a.bcd'; - - test('isValidVersion should be true', () => { - expect(Utils.isValidVersion(validVersion)).toBeTruthy(); - }); - test('isValidVersion should be false', () => { - expect(Utils.isValidVersion(invalidVersion)).toBeFalsy(); - }); -}); - -describe('Testing setPSModulePath', () => { - test('PSModulePath with azPSVersion non-empty', () => { - Utils.setPSModulePath(version); - expect(process.env.PSModulePath).toContain(version); - }); - test('PSModulePath with azPSVersion empty', () => { - const prevPSModulePath = process.env.PSModulePath; - Utils.setPSModulePath(); - expect(process.env.PSModulePath).not.toEqual(prevPSModulePath); - }); -}); - -describe('Testing getLatestModule', () => { - let getLatestModuleSpy; - - beforeEach(() => { - getLatestModuleSpy = jest.spyOn(Utils, 'getLatestModule'); - }); - test('getLatestModule should pass', async () => { - getLatestModuleSpy.mockImplementationOnce((_moduleName: string) => Promise.resolve(version)); - await Utils.getLatestModule(moduleName); - expect(getLatestModuleSpy).toHaveBeenCalled(); - }); -}); diff --git a/action.yml b/action.yml index c9883bb8..77aba20b 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ # Login to Azure subscription name: 'Azure Login' -description: 'Authenticate to Azure using OIDC and run your Az CLI or Az PowerShell based actions or scripts. github.com/Azure/Actions' +description: 'Authenticate to Azure and run your Azure CLI or Azure PowerShell based actions or scripts.' inputs: creds: description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS' @@ -15,7 +15,7 @@ inputs: description: 'Azure subscriptionId' required: false enable-AzPSSession: - description: 'Set this value to true to enable Azure PowerShell Login in addition to Az CLI login' + description: 'Set this value to true to enable Azure PowerShell Login in addition to Azure CLI login' required: false default: false environment: @@ -30,6 +30,10 @@ inputs: description: 'Provide audience field for access-token. Default value is api://AzureADTokenExchange' required: false default: 'api://AzureADTokenExchange' + auth-type: + description: 'The type of authentication. Supported values are SERVICE_PRINCIPAL, IDENTITY. Default value is SERVICE_PRINCIPAL' + required: false + default: 'SERVICE_PRINCIPAL' branding: icon: 'login.svg' color: 'blue' diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index adf86efe..03a86f20 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -7,14 +7,20 @@ import * as io from '@actions/io'; export class AzureCliLogin { loginConfig: LoginConfig; azPath: string; - + loginOptions: ExecOptions; + constructor(loginConfig: LoginConfig) { this.loginConfig = loginConfig; + this.loginOptions = defaultExecOptions(); } async login() { + core.info(`Running Azure CLI Login.`); this.azPath = await io.which("az", true); - core.debug(`az cli path: ${this.azPath}`); + if (!this.azPath) { + throw new Error("Azure CLI is not found in the runner."); + } + core.debug(`Azure CLI path: ${this.azPath}`); let output: string = ""; const execOptions: any = { @@ -24,39 +30,35 @@ export class AzureCliLogin { } } }; - await this.executeAzCliCommand("--version", true, execOptions); - core.debug(`az cli version used:\n${output}`); + + await this.executeAzCliCommand(["--version"], true, execOptions); + core.debug(`Azure CLI version used:\n${output}`); this.setAzurestackEnvIfNecessary(); - await this.executeAzCliCommand(`cloud set -n "${this.loginConfig.environment}"`, false); - console.log(`Done setting cloud: "${this.loginConfig.environment}"`); + await this.executeAzCliCommand(["cloud", "set", "-n", this.loginConfig.environment], false); + core.info(`Done setting cloud: "${this.loginConfig.environment}"`); - // Attempting Az cli login - var commonArgs = ["--service-principal", - "-u", this.loginConfig.servicePrincipalId, - "--tenant", this.loginConfig.tenantId - ]; - if (this.loginConfig.allowNoSubscriptionsLogin) { - commonArgs = commonArgs.concat("--allow-no-subscriptions"); - } - if (this.loginConfig.enableOIDC) { - commonArgs = commonArgs.concat("--federated-token", this.loginConfig.federatedToken); + if (this.loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { + let args = ["--service-principal", + "--username", this.loginConfig.servicePrincipalId, + "--tenant", this.loginConfig.tenantId + ]; + if (this.loginConfig.servicePrincipalSecret) { + await this.loginWithSecret(args); + } + else { + await this.loginWithOIDC(args); + } } else { - console.log("Note: Azure/login action also supports OIDC login mechanism. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.") - commonArgs = commonArgs.concat(`--password=${this.loginConfig.servicePrincipalKey}`); - } - - const loginOptions: ExecOptions = defaultExecOptions(); - await this.executeAzCliCommand(`login`, true, loginOptions, commonArgs); - - if (!this.loginConfig.allowNoSubscriptionsLogin) { - var args = [ - "--subscription", - this.loginConfig.subscriptionId - ]; - await this.executeAzCliCommand(`account set`, true, loginOptions, args); + let args = ["--identity"]; + if (this.loginConfig.servicePrincipalId) { + await this.loginWithUserAssignedIdentity(args); + } + else { + await this.loginWithSystemAssignedIdentity(args); + } } } @@ -68,16 +70,16 @@ export class AzureCliLogin { throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined."); } - console.log(`Unregistering cloud: "${this.loginConfig.environment}" first if it exists`); + core.info(`Unregistering cloud: "${this.loginConfig.environment}" first if it exists`); try { - await this.executeAzCliCommand(`cloud set -n AzureCloud`, true); - await this.executeAzCliCommand(`cloud unregister -n "${this.loginConfig.environment}"`, false); + await this.executeAzCliCommand(["cloud", "set", "-n", "AzureCloud"], true); + await this.executeAzCliCommand(["cloud", "unregister", "-n", this.loginConfig.environment], false); } catch (error) { - console.log(`Ignore cloud not registered error: "${error}"`); + core.info(`Ignore cloud not registered error: "${error}"`); } - console.log(`Registering cloud: "${this.loginConfig.environment}" with ARM endpoint: "${this.loginConfig.resourceManagerEndpointUrl}"`); + core.info(`Registering cloud: "${this.loginConfig.environment}" with ARM endpoint: "${this.loginConfig.resourceManagerEndpointUrl}"`); try { let baseUri = this.loginConfig.resourceManagerEndpointUrl; if (baseUri.endsWith('/')) { @@ -86,22 +88,63 @@ export class AzureCliLogin { let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with . let suffixStorage = baseUri.substring(baseUri.indexOf('.') + 1); // storage suffix starts without . let profileVersion = "2019-03-01-hybrid"; - await this.executeAzCliCommand(`cloud register -n "${this.loginConfig.environment}" --endpoint-resource-manager "${this.loginConfig.resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false); + await this.executeAzCliCommand(["cloud", "register", "-n", this.loginConfig.environment, "--endpoint-resource-manager", `"${this.loginConfig.resourceManagerEndpointUrl}"`, "--suffix-keyvault-dns", `"${suffixKeyvault}"`, "--suffix-storage-endpoint", `"${suffixStorage}"`, "--profile", `"${profileVersion}"`], false); } catch (error) { - core.error(`Error while trying to register cloud "${this.loginConfig.environment}": "${error}"`); + core.error(`Error while trying to register cloud "${this.loginConfig.environment}"`); + throw error; } - console.log(`Done registering cloud: "${this.loginConfig.environment}"`) + core.info(`Done registering cloud: "${this.loginConfig.environment}"`) + } + + async loginWithSecret(args: string[]) { + core.info("Note: Azure/login action also supports OIDC login mechanism. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.") + args.push(`--password=${this.loginConfig.servicePrincipalSecret}`); + await this.callCliLogin(args, 'service principal with secret'); + } + + async loginWithOIDC(args: string[]) { + await this.loginConfig.getFederatedToken(); + args.push("--federated-token", this.loginConfig.federatedToken); + await this.callCliLogin(args, 'OIDC'); + } + + async loginWithUserAssignedIdentity(args: string[]) { + args.push("--username", this.loginConfig.servicePrincipalId); + await this.callCliLogin(args, 'user-assigned managed identity'); + } + + async loginWithSystemAssignedIdentity(args: string[]) { + await this.callCliLogin(args, 'system-assigned managed identity'); + } + + async callCliLogin(args: string[], methodName: string) { + core.info(`Attempting Azure CLI login by using ${methodName}...`); + args.unshift("login"); + if (this.loginConfig.allowNoSubscriptionsLogin) { + args.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand(args, true, this.loginOptions); + await this.setSubscription(); + core.info(`Azure CLI login succeeds by using ${methodName}.`); + } + + async setSubscription() { + if (this.loginConfig.allowNoSubscriptionsLogin) { + return; + } + let args = ["account", "set", "--subscription", this.loginConfig.subscriptionId]; + await this.executeAzCliCommand(args, true, this.loginOptions); + core.info("Subscription is set successfully."); } async executeAzCliCommand( - command: string, + args: string[], silent?: boolean, - execOptions: any = {}, - args: any = []) { + execOptions: any = {}) { execOptions.silent = !!silent; - await exec.exec(`"${this.azPath}" ${command}`, args, execOptions); + await exec.exec(`"${this.azPath}"`, args, execOptions); } } @@ -119,7 +162,7 @@ function defaultExecOptions(): exec.ExecOptions { //removing the keyword 'ERROR' to avoid duplicates while throwing error error = error.slice(5); } - core.setFailed(error); + core.error(error); } } } diff --git a/src/PowerShell/AzPSConstants.ts b/src/PowerShell/AzPSConstants.ts new file mode 100644 index 00000000..d0b569e1 --- /dev/null +++ b/src/PowerShell/AzPSConstants.ts @@ -0,0 +1,7 @@ +export default class AzPSConstants { + static readonly DEFAULT_AZ_PATH_ON_LINUX: string = '/usr/share'; + static readonly DEFAULT_AZ_PATH_ON_WINDOWS: string = 'C:\\Modules'; + static readonly AzAccounts: string = "Az.Accounts"; + static readonly PowerShell_CmdName = "pwsh"; +} + diff --git a/src/PowerShell/AzPSLogin.ts b/src/PowerShell/AzPSLogin.ts new file mode 100644 index 00000000..c14f327e --- /dev/null +++ b/src/PowerShell/AzPSLogin.ts @@ -0,0 +1,100 @@ +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; + } +} + diff --git a/src/PowerShell/AzPSScriptBuilder.ts b/src/PowerShell/AzPSScriptBuilder.ts new file mode 100644 index 00000000..81112f3d --- /dev/null +++ b/src/PowerShell/AzPSScriptBuilder.ts @@ -0,0 +1,113 @@ +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; + } +} + diff --git a/src/PowerShell/Constants.ts b/src/PowerShell/Constants.ts deleted file mode 100644 index 543226df..00000000 --- a/src/PowerShell/Constants.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default class Constants { - static readonly prefix: string = "az_"; - static readonly moduleName: string = "Az.Accounts"; - static readonly versionPattern = /[0-9]+\.[0-9]+\.[0-9]+/; - - static readonly AzureCloud: string = "AzureCloud"; - static readonly Subscription: string = "Subscription"; - static readonly ServicePrincipal: string = "ServicePrincipal"; - - static readonly Success: string = "Success"; - static readonly Error: string = "Error"; - static readonly AzVersion: string = "AzVersion"; -} diff --git a/src/PowerShell/IAzurePowerShellSession.ts b/src/PowerShell/IAzurePowerShellSession.ts deleted file mode 100644 index 544369d5..00000000 --- a/src/PowerShell/IAzurePowerShellSession.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface IAzurePowerShellSession { - initialize(); - login(); -} \ No newline at end of file diff --git a/src/PowerShell/ServicePrincipalLogin.ts b/src/PowerShell/ServicePrincipalLogin.ts deleted file mode 100644 index e9b12ac7..00000000 --- a/src/PowerShell/ServicePrincipalLogin.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as core from '@actions/core'; - -import Utils from './Utilities/Utils'; -import PowerShellToolRunner from './Utilities/PowerShellToolRunner'; -import ScriptBuilder from './Utilities/ScriptBuilder'; -import Constants from './Constants'; -import { LoginConfig } from '../common/LoginConfig'; - -export class ServicePrincipalLogin implements IAzurePowerShellSession { - static readonly scopeLevel: string = Constants.Subscription; - static readonly scheme: string = Constants.ServicePrincipal; - loginConfig: LoginConfig; - - constructor(loginConfig: LoginConfig) { - this.loginConfig = loginConfig; - } - - async initialize() { - Utils.setPSModulePath(); - const azLatestVersion: string = await Utils.getLatestModule(Constants.moduleName); - core.debug(`Az Module version used: ${azLatestVersion}`); - Utils.setPSModulePath(`${Constants.prefix}${azLatestVersion}`); - } - - async login() { - let output: string = ""; - let commandStdErr = false; - const options: any = { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - }, - stderr: (data: Buffer) => { - let error = data.toString(); - if (error && error.trim().length !== 0) { - commandStdErr = true; - core.error(error); - } - } - } - }; - const args: any = { - servicePrincipalId: this.loginConfig.servicePrincipalId, - servicePrincipalKey: this.loginConfig.servicePrincipalKey, - federatedToken: this.loginConfig.federatedToken, - subscriptionId: this.loginConfig.subscriptionId, - environment: this.loginConfig.environment, - scopeLevel: ServicePrincipalLogin.scopeLevel, - allowNoSubscriptionsLogin: this.loginConfig.allowNoSubscriptionsLogin, - resourceManagerEndpointUrl: this.loginConfig.resourceManagerEndpointUrl - } - const script: string = new ScriptBuilder().getAzPSLoginScript(ServicePrincipalLogin.scheme, this.loginConfig.tenantId, args); - await PowerShellToolRunner.init(); - await PowerShellToolRunner.executePowerShellScriptBlock(script, options); - const result: any = JSON.parse(output.trim()); - if (!(Constants.Success in result)) { - throw new Error(`Azure PowerShell login failed with error: ${result[Constants.Error]}`); - } - console.log(`Azure PowerShell session successfully initialized`); - } - -} \ No newline at end of file diff --git a/src/PowerShell/Utilities/PowerShellToolRunner.ts b/src/PowerShell/Utilities/PowerShellToolRunner.ts deleted file mode 100644 index 3bc01de4..00000000 --- a/src/PowerShell/Utilities/PowerShellToolRunner.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as io from '@actions/io'; -import * as exec from '@actions/exec'; - -export default class PowerShellToolRunner { - static psPath: string; - static async init() { - if(!PowerShellToolRunner.psPath) { - PowerShellToolRunner.psPath = await io.which("pwsh", true); - } - } - - static async executePowerShellScriptBlock(scriptBlock: string, options: any = {}) { - //Options for error handling - await exec.exec(`"${PowerShellToolRunner.psPath}" -Command`, [scriptBlock], options) - } -} \ No newline at end of file diff --git a/src/PowerShell/Utilities/ScriptBuilder.ts b/src/PowerShell/Utilities/ScriptBuilder.ts deleted file mode 100644 index 9b1a0e7d..00000000 --- a/src/PowerShell/Utilities/ScriptBuilder.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as core from '@actions/core'; - -import Constants from "../Constants"; - -export default class ScriptBuilder { - script: string = ""; - - getAzPSLoginScript(scheme: string, tenantId: string, args: any): string { - let command = `Clear-AzContext -Scope Process; - Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; - - if (scheme === Constants.ServicePrincipal) { - - if (args.environment.toLowerCase() == "azurestack") { - command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; - } - // Separate command script for OIDC and non-OIDC - if (!!args.federatedToken) { - command += `Connect-AzAccount -ServicePrincipal -ApplicationId '${args.servicePrincipalId}' -Tenant '${tenantId}' -FederatedToken '${args.federatedToken}' \ - -Environment '${args.environment}' | out-null;`; - } - else { - command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \ - (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \ - -Environment '${args.environment}' | out-null;`; - } - // command to set the subscription - if (args.scopeLevel === Constants.Subscription && !args.allowNoSubscriptionsLogin) { - command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`; - } - } - - this.script += `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - ${command} - $output['${Constants.Success}'] = "true" - } - catch { - $output['${Constants.Error}'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - core.debug(`Azure PowerShell Login Script: ${this.script}`); - return this.script; - } - - getLatestModuleScript(moduleName: string): string { - const command: string = `Get-Module -Name ${moduleName} -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1`; - this.script += `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - $data = ${command} - $output['${Constants.AzVersion}'] = $data.Version.ToString() - $output['${Constants.Success}'] = "true" - } - catch { - $output['${Constants.Error}'] = $_.exception.Message - } - return ConvertTo-Json $output`; - core.debug(`GetLatestModuleScript: ${this.script}`); - return this.script; - } - -} \ No newline at end of file diff --git a/src/PowerShell/Utilities/Utils.ts b/src/PowerShell/Utilities/Utils.ts deleted file mode 100644 index 8814ab1f..00000000 --- a/src/PowerShell/Utilities/Utils.ts +++ /dev/null @@ -1,61 +0,0 @@ -import * as os from 'os'; - -import Constants from '../Constants'; -import ScriptBuilder from './ScriptBuilder'; -import PowerShellToolRunner from './PowerShellToolRunner'; - -export default class Utils { - /** - * Add the folder path where Az modules are present to PSModulePath based on runner - * @param azPSVersion - * If azPSVersion is empty, folder path in which all Az modules are present are set - * If azPSVersion is not empty, folder path of exact Az module version is set - */ - static setPSModulePath(azPSVersion: string = "") { - let modulePath: string = ""; - const runner: string = process.env.RUNNER_OS || os.type(); - switch (runner.toLowerCase()) { - case "linux": - modulePath = `/usr/share/${azPSVersion}:`; - break; - case "windows": - case "windows_nt": - modulePath = `C:\\Modules\\${azPSVersion};`; - break; - case "macos": - case "darwin": - throw new Error(`OS not supported`); - default: - throw new Error(`Unknown os: ${runner.toLowerCase()}`); - } - process.env.PSModulePath = `${modulePath}${process.env.PSModulePath}`; - } - - static async getLatestModule(moduleName: string): Promise { - let output: string = ""; - const options: any = { - listeners: { - stdout: (data: Buffer) => { - output += data.toString(); - } - } - }; - await PowerShellToolRunner.init(); - await PowerShellToolRunner.executePowerShellScriptBlock(new ScriptBuilder() - .getLatestModuleScript(moduleName), options); - const result = JSON.parse(output.trim()); - if (!(Constants.Success in result)) { - throw new Error(result[Constants.Error]); - } - const azLatestVersion: string = result[Constants.AzVersion]; - if (!Utils.isValidVersion(azLatestVersion)) { - throw new Error(`Invalid AzPSVersion: ${azLatestVersion}`); - } - return azLatestVersion; - } - - static isValidVersion(version: string): boolean { - return !!version.match(Constants.versionPattern); - } -} - diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index 914e5300..be03bb60 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -2,6 +2,8 @@ import * as core from '@actions/core'; import { FormatType, SecretParser } from 'actions-secret-parser'; export class LoginConfig { + static readonly AUTH_TYPE_SERVICE_PRINCIPAL = "SERVICE_PRINCIPAL"; + static readonly AUTH_TYPE_IDENTITY = "IDENTITY"; static readonly azureSupportedCloudName = new Set([ "azureusgovernment", "azurechinacloud", @@ -9,78 +11,102 @@ export class LoginConfig { "azurecloud", "azurestack"]); + static readonly azureSupportedAuthType = new Set([ + LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL, + LoginConfig.AUTH_TYPE_IDENTITY]); + + authType: string; servicePrincipalId: string; - servicePrincipalKey: string; + servicePrincipalSecret: string; tenantId: string; subscriptionId: string; resourceManagerEndpointUrl: string; allowNoSubscriptionsLogin: boolean; - enableOIDC: boolean; environment: string; enableAzPSSession: boolean; audience: string; federatedToken: string; - constructor() { - this.enableOIDC = true; - } - async initialize() { this.environment = core.getInput("environment").toLowerCase(); this.enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; this.allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; + this.authType = core.getInput('auth-type').toUpperCase(); this.servicePrincipalId = core.getInput('client-id', { required: false }); - this.servicePrincipalKey = null; + this.servicePrincipalSecret = null; this.tenantId = core.getInput('tenant-id', { required: false }); this.subscriptionId = core.getInput('subscription-id', { required: false }); + this.readParametersFromCreds(); + this.audience = core.getInput('audience', { required: false }); this.federatedToken = null; - let creds = core.getInput('creds', { required: false }); - let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null; - if (creds) { - core.debug('using creds JSON...'); - this.enableOIDC = false; - this.servicePrincipalId = secrets.getSecret("$.clientId", true); - this.servicePrincipalKey = secrets.getSecret("$.clientSecret", true); - this.tenantId = secrets.getSecret("$.tenantId", true); - this.subscriptionId = secrets.getSecret("$.subscriptionId", true); - this.resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); - } - this.getFederatedTokenIfNecessary(); + this.mask(this.servicePrincipalId); + this.mask(this.servicePrincipalSecret); } - async getFederatedTokenIfNecessary() { - if (!this.enableOIDC) { + private readParametersFromCreds() { + let creds = core.getInput('creds', { required: false }); + let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null; + if (!secrets) { return; } + + if(this.authType != LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL){ + return; + } + + if (this.servicePrincipalId || this.tenantId || this.subscriptionId) { + core.warning("At least one of the parameters 'client-id', 'subscription-id' or 'tenant-id' is set. 'creds' will be ignored."); + return; + } + + core.debug('Reading creds in JSON...'); + this.servicePrincipalId = this.servicePrincipalId ? this.servicePrincipalId : secrets.getSecret("$.clientId", false); + this.servicePrincipalSecret = secrets.getSecret("$.clientSecret", false); + this.tenantId = this.tenantId ? this.tenantId : secrets.getSecret("$.tenantId", false); + this.subscriptionId = this.subscriptionId ? this.subscriptionId : secrets.getSecret("$.subscriptionId", false); + this.resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); + if (!this.servicePrincipalId || !this.servicePrincipalSecret || !this.tenantId || !this.subscriptionId) { + throw new Error("Not all parameters are provided in 'creds'. Double-check if all keys are defined in 'creds': 'clientId', 'clientSecret', 'subscriptionId', 'tenantId'."); + } + } + + async getFederatedToken() { try { this.federatedToken = await core.getIDToken(this.audience); + this.mask(this.federatedToken); } catch (error) { core.error(`Please make sure to give write permissions to id-token in the workflow.`); throw error; } - if (!!this.federatedToken) { - let [issuer, subjectClaim] = await jwtParser(this.federatedToken); - console.log("Federated token details: \n issuer - " + issuer + " \n subject claim - " + subjectClaim); + let [issuer, subjectClaim] = await jwtParser(this.federatedToken); + core.info("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim); + } + + validate() { + if (!LoginConfig.azureSupportedCloudName.has(this.environment)) { + throw new Error(`Unsupported value '${this.environment}' for environment is passed. The list of supported values for environment are '${Array.from(LoginConfig.azureSupportedCloudName).join("', '")}'. `); } - else { - throw new Error("Failed to fetch federated token."); + if (!LoginConfig.azureSupportedAuthType.has(this.authType)) { + throw new Error(`Unsupported value '${this.authType}' for authentication type is passed. The list of supported values for auth-type are '${Array.from(LoginConfig.azureSupportedAuthType).join("', '")}'.`); + } + if (this.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { + if (!this.servicePrincipalId || !this.tenantId) { + throw new Error(`Using auth-type: ${LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL}. Not all values are present. Ensure 'client-id' and 'tenant-id' are supplied.`); + } + } + if (!this.subscriptionId && !this.allowNoSubscriptionsLogin) { + throw new Error("Ensure subscriptionId is supplied."); } } - async validate() { - if (!this.servicePrincipalId || !this.tenantId || !(this.servicePrincipalKey || this.enableOIDC)) { - throw new Error("Not all values are present in the credentials. Ensure clientId, clientSecret and tenantId are supplied."); - } - if (!this.subscriptionId && !this.allowNoSubscriptionsLogin) { - throw new Error("Not all values are present in the credentials. Ensure subscriptionId is supplied."); - } - if (!LoginConfig.azureSupportedCloudName.has(this.environment)) { - throw new Error("Unsupported value for environment is passed.The list of supported values for environment are ‘azureusgovernment', ‘azurechinacloud’, ‘azuregermancloud’, ‘azurecloud’ or ’azurestack’"); + mask(parameterValue: string) { + if (parameterValue) { + core.setSecret(parameterValue); } } } @@ -90,4 +116,5 @@ async function jwtParser(federatedToken: string) { let bufferObj = Buffer.from(tokenPayload, "base64"); let decodedPayload = JSON.parse(bufferObj.toString("utf8")); return [decodedPayload['iss'], decodedPayload['sub']]; -} \ No newline at end of file +} + diff --git a/src/main.ts b/src/main.ts index 3848ed4d..95f2d6a4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import * as core from '@actions/core'; -import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin'; +import { AzPSLogin } from './PowerShell/AzPSLogin'; import { LoginConfig } from './common/LoginConfig'; import { AzureCliLogin } from './Cli/AzureCliLogin'; @@ -7,7 +7,6 @@ var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USE var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT}` : ""; async function main() { - var isAzCLISuccess = false; try { let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`; let actionName = 'AzureLogin'; @@ -16,33 +15,24 @@ async function main() { core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); - // perpare the login configuration + // prepare the login configuration var loginConfig = new LoginConfig(); await loginConfig.initialize(); await loginConfig.validate(); - // login to Azure Cli + // login to Azure CLI var cliLogin = new AzureCliLogin(loginConfig); await cliLogin.login(); - isAzCLISuccess = true; //login to Azure PowerShell if (loginConfig.enableAzPSSession) { - console.log(`Running Azure PS Login`); - var spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(loginConfig); - await spnlogin.initialize(); - await spnlogin.login(); + var psLogin: AzPSLogin = new AzPSLogin(loginConfig); + await psLogin.login(); } - - console.log("Login successful."); } catch (error) { - if (!isAzCLISuccess) { - core.setFailed(`Az CLI Login failed with ${error}. Please check the credentials and make sure az is installed on the runner. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows`); - } - else { - core.setFailed(`Azure PowerShell Login failed with ${error}. Please check the credentials and make sure az is installed on the runner. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows`); - } + core.setFailed(`Login 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. Double check if the 'auth-type' is correct. Refer to https://github.com/Azure/login#readme for more information.`); + core.debug(error.stack); } finally { // Reset AZURE_HTTP_USER_AGENT @@ -52,3 +42,4 @@ async function main() { } main(); +