From a7866ef2bb9049bcd7e9a4e618e038dbe1b52344 Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Thu, 8 Jun 2023 13:53:04 +0800 Subject: [PATCH 1/8] support managed-identity-cli --- .github/workflows/azure-login-negative.yml | 17 +++ .github/workflows/azure-login-positive.yml | 118 ++++++++++++++---- README.md | 72 ++++++++++- src/Cli/AzureCliLogin.ts | 134 +++++++++++++++------ src/common/LoginConfig.ts | 35 ++---- src/main.ts | 6 +- 6 files changed, 287 insertions(+), 95 deletions(-) diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml index 42e2e3bc4..2706180dc 100644 --- a/.github/workflows/azure-login-negative.yml +++ b/.github/workflows/azure-login-negative.yml @@ -354,6 +354,23 @@ jobs: - name: Check Last step failed if: steps.login_10.outcome == 'success' uses: actions/github-script@v3 + with: + script: | + 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.') \ No newline at end of file diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml index a2a1b04dd..2a829db21 100644 --- a/.github/workflows/azure-login-positive.yml +++ b/.github/workflows/azure-login-positive.yml @@ -62,18 +62,18 @@ jobs: tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true - enable-AzPSSession: true + # enable-AzPSSession: true - name: Run Azure Cli again run: | az account show - - name: Run Azure PowerShell again - uses: azure/powershell@v1.2.0 - with: - azPSVersion: "latest" - inlineScript: | - Get-AzContext | Format-List + # - name: Run Azure PowerShell again + # uses: azure/powershell@v1.2.0 + # with: + # azPSVersion: "latest" + # inlineScript: | + # Get-AzContext | Format-List ParameterTest: strategy: @@ -105,21 +105,6 @@ jobs: subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} enable-AzPSSession: true - - name: Run Azure Cli - run: | - az account show - az group show --name GitHubActionGroup - az vm list - - - name: Run Azure PowerShell - uses: azure/powershell@v1.2.0 - with: - azPSVersion: "latest" - inlineScript: | - Get-AzContext | Format-List - Get-AzResourceGroup -Name GitHubActionGroup - Get-AzVM - - name: Login with creds, disable ps session uses: ./ with: @@ -185,3 +170,92 @@ 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 + uses: ./ + # with: + # 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 + uses: ./ + with: + client-id: ${{ secrets.UMI1_CLIENT_ID }} + # 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 }} + # 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 tenant-level user-assigned managed identity with allow-no-subscriptions + uses: ./ + with: + client-id: ${{ secrets.UMI2_CLIENT_ID }} + 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 \ No newline at end of file diff --git a/README.md b/README.md index 0bbfe4d9a..b1daf66b5 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,27 @@ With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Ac - By default, the action only logs in with the Azure CLI (using the `az login` command). To log in with the Az PowerShell module, set `enable-AzPSSession` to true. To login to Azure tenants without any subscriptions, set the optional parameter `allow-no-subscriptions` to true. +- About the parameter `subscription-id`: This parameter is used to specify which subscription to work. If you don't specify a subscription, the Action uses your current, active subscription. + - To login into one of the Azure Government clouds or Azure Stack, set the optional parameter `environment` with one of the supported values `AzureUSGovernment` or `AzureChinaCloud` or `AzureStack`. If this parameter is not specified, it takes the default value `AzureCloud` and connects to the Azure Public Cloud. Additionally, the parameter `creds` takes the Azure service principal created in the particular cloud to connect (Refer to the [Configure a service principal with a secret](#configure-a-service-principal-with-a-secret) section below for details). -- The Action supports two different ways of authentication with Azure. One using the Azure Service Principal with secrets. The other is OpenID connect (OIDC) method of authentication using Azure [Workload Identity Federation](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation). **We recommend using OIDC based authentication for increased security.** + +- The Action supports three different ways of authentication with Azure. + 1. Using the Azure Service Principal with secrets. + 2. Using OpenID connect (OIDC) method of authentication by Azure [Workload Identity Federation](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation). **We recommend using OIDC based authentication for increased security.** + 3. Using the Managed Identity configued on an Azure VM. + - To login using Azure Service Principal with a secret, follow [this](#configure-a-service-principal-with-a-secret) guidance. - To login using **OpenID Connect (OIDC) based Federated Identity Credentials**, you need to first configure trust between GitHub workflow and an Azure Managed Identity or an Azure AD App (Service Principal) 1. Follow [this](#configure-a-federated-credential-to-use-oidc-based-authentication) guidance to create a Federated Credential associated with your Azure Managed Identity or AD App (Service Principal). This is needed to establish OIDC trust between GitHub deployment workflows and the specific Azure resources scoped by the Managed Identity/service principal. 2. In your GitHub workflow, Set `permissions:` with `id-token: write` at workflow level or job level based on whether the OIDC token needs to be auto-generated for all Jobs or a specific Job. 3. Within the Job deploying to Azure, add Azure/login action and pass the `client-id` and `tenant-id` of the Azure Managed Identity/service principal associated with an OIDC Federated Identity Credential created in step (i). You also need to pass `subscription-id` or set `allow-no-subscriptions` to true. +- To login using Managed Identities, follow [this](#configure-azure-managed-identities-with-self-hosted-runners) guidance. + +- The Action identifies the authentication method by the parameters you input. + 1. If the input is `creds` with all the parameters: `clientId`, `tenantId` and `clientSecret` are detected in your input, we will attempt to login by using service principal with the secret. + 2. If parameters: `clientId` and `tenantId` are detected in your input, we will attempt to login by using OIDC. + 3. If parameters: `clientId` is detected in your input, we will attempt to login by using user-assigned managed identity. + 4. If no parameter is detected in your input, we will attempt to login by using system-assigned managed identity. Note: @@ -102,7 +116,6 @@ jobs: with: azcliversion: latest inlineScript: | - az account show az group list ``` @@ -143,6 +156,44 @@ jobs: Refer to the [Azure PowerShell](https://github.com/azure/powershell) GitHub Action to run your Azure PowerShell scripts. +## Sample workflow that uses Azure login action with system-assigned managed identity + +```yaml +# File: .github/workflows/workflow.yml +on: [push] +name: Azure System-assigned Managed Identity sample +jobs: + build-and-deploy: + runs-on: self-hosted + steps: + - name: Azure Login + uses: azure/login@v1 + + - name: Show group list + run: | + az group list +``` + +## Sample workflow that uses Azure login action with user-assigned managed identity + +```yaml +# File: .github/workflows/workflow.yml +on: [push] +name: Azure User-assigned Managed Identity sample +jobs: + build-and-deploy: + runs-on: self-hosted + steps: + - name: Azure Login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Show group list + run: | + az group list +``` + ## Sample to connect to Azure US Government cloud ```yaml @@ -196,7 +247,7 @@ Refer to the [Azure Stack Hub Login Action Tutorial](https://learn.microsoft.com For using any credentials like Azure Service Principal, Publish Profile etc add them as [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the GitHub repository and then use them in the workflow. -Follow the following steps to configure Azure Service Principal with a secret: +Follow the following steps to configure Azure Service Principal with a secret in the scope of `resource-group` as the role of `contributor`. - Define a new secret under your repository settings, Add secret menu - Store the output of the below [az cli](https://learn.microsoft.com/cli/azure/?view=azure-cli-latest) command as the value of secret variable, for example 'AZURE_CREDENTIALS' @@ -208,7 +259,7 @@ Follow the following steps to configure Azure Service Principal with a secret: --sdk-auth ``` -Replace `{subscription-id}` and `{resource-group}` with the subscription and resource group details, respectively. +Please assign the service principal with proper `role` and `scope` you desired. Replace `{subscription-id}` and `{resource-group}` with the subscription and resource group details, respectively. The command should output a JSON object similar to this: @@ -242,13 +293,22 @@ If you already created and assigned a Service Principal in Azure you can manuall ### Configure a Federated Credential to use OIDC based authentication -Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app”](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider (preview) which has more details about the Azure Workload Identity Federation (OIDC) support. +Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a federated identity credential on user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider (preview) which has more details about the Azure Workload Identity Federation (OIDC) support. You can add federated credentials in the Azure portal or with the Microsoft Graph REST API. +### Configure Azure Managed Identities with self-hosted runners + +If you want to use managed identities (system- or user-assigned) to sign in, a self-hosted Github runner is required to be installed on an Azure VM with a managed identity configured. Please refer to Microsoft's documentation at ["Configure managed identities for Azure resources on an Azure VM"](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-cli-windows-vm). To use system-assigned managed identity, there are no mandatory parameters. To use user-assigned managed identity, `client-id` of the identity is mandatory. +To get the `client-id` of a user-assigned managed identity, use the command: + +```bash +az vm identity show --resource-group --name --query userAssignedIdentities +``` + ## Support for using `allow-no-subscriptions` flag with az login -Capability has been added to support access to tenants without subscriptions for both OIDC and non-OIDC. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default. +Capability has been added to support access to tenants without subscriptions. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default. ```yaml # File: .github/workflows/workflow.yml diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index adf86efe2..0d775c665 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -7,13 +7,20 @@ import * as io from '@actions/io'; export class AzureCliLogin { loginConfig: LoginConfig; azPath: string; - + loginOptions: ExecOptions; + isSuccess: boolean; + constructor(loginConfig: LoginConfig) { this.loginConfig = loginConfig; + this.loginOptions = defaultExecOptions(); + this.isSuccess = false; } async login() { this.azPath = await io.which("az", true); + if (!this.azPath) { + throw new Error("az cli is not found in the runner."); + } core.debug(`az cli path: ${this.azPath}`); let output: string = ""; @@ -24,40 +31,19 @@ export class AzureCliLogin { } } }; - await this.executeAzCliCommand("--version", true, execOptions); + + await this.executeAzCliCommand('--version', [], true, execOptions); core.debug(`az cli version used:\n${output}`); this.setAzurestackEnvIfNecessary(); - await this.executeAzCliCommand(`cloud set -n "${this.loginConfig.environment}"`, false); + await this.executeAzCliCommand('cloud set', ['-n', `${this.loginConfig.environment}`], false); console.log(`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); - } - 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); - } + await this.loginWithSecret(); + await this.loginWithOIDC(); + await this.loginWithUserManagedIdentity(); + await this.loginWithSystemManagedIdentity(); } async setAzurestackEnvIfNecessary() { @@ -70,8 +56,8 @@ export class AzureCliLogin { console.log(`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}"`); @@ -86,7 +72,7 @@ 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}"`); @@ -95,11 +81,90 @@ export class AzureCliLogin { console.log(`Done registering cloud: "${this.loginConfig.environment}"`) } + async loginWithSecret() { + if (!(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId && this.loginConfig.servicePrincipalKey) || this.isSuccess) { + return; + } + console.log('Attempting az cli login by using service principal with secret...\nNote: Azure/login action also supports OIDC login mechanism. If you want to use OIDC login, please do not input ClientSecret. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.'); + let commonArgs = ["--service-principal", + "-u", this.loginConfig.servicePrincipalId, + "--tenant", this.loginConfig.tenantId, + "-p", this.loginConfig.servicePrincipalKey + ]; + if (this.loginConfig.allowNoSubscriptionsLogin) { + commonArgs.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + } + + async loginWithOIDC() { + if (!(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId) || this.isSuccess) { + return; + } + console.log('Attempting az cli login by using OIDC...'); + await this.loginConfig.getFederatedToken(); + let commonArgs = ["--service-principal", + "-u", this.loginConfig.servicePrincipalId, + "--tenant", this.loginConfig.tenantId, + "--federated-token", this.loginConfig.federatedToken + ]; + if (this.loginConfig.allowNoSubscriptionsLogin) { + commonArgs.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + } + + async loginWithUserManagedIdentity() { + if (!this.loginConfig.servicePrincipalId || this.isSuccess) { + return; + } + console.log('Attempting az cli login by using user-assigned managed identity...'); + let commonArgs = ["--identity", + "-u", this.loginConfig.servicePrincipalId]; + if (this.loginConfig.allowNoSubscriptionsLogin) { + commonArgs.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + } + + async loginWithSystemManagedIdentity() { + if (this.isSuccess) { + return; + } + console.log('Attempting az cli login by using system-assigned managed identity...'); + let commonArgs = ["--identity"]; + if (this.loginConfig.allowNoSubscriptionsLogin) { + commonArgs.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + } + + async setSubscription() { + if (this.loginConfig.allowNoSubscriptionsLogin) { + return; + } + if (!this.loginConfig.subscriptionId) { + core.warning('No subscription-id is given. Skip setting subscription...If there are mutiple subscriptions under the tenant, please input subscription-id to specify which subscription to use.'); + return; + } + let args = ["--subscription", this.loginConfig.subscriptionId]; + await this.executeAzCliCommand('account set', args, true, this.loginOptions); + console.log('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); } @@ -125,4 +190,3 @@ function defaultExecOptions(): exec.ExecOptions { } }; } - diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index 914e53009..7bda2389a 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -15,16 +15,11 @@ export class LoginConfig { 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"; @@ -35,27 +30,22 @@ export class LoginConfig { this.tenantId = core.getInput('tenant-id', { required: false }); this.subscriptionId = core.getInput('subscription-id', { required: false }); - 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.audience = core.getInput('audience', { required: false }); + this.federatedToken = null; } - async getFederatedTokenIfNecessary() { - if (!this.enableOIDC) { - return; - } + async getFederatedToken() { try { this.federatedToken = await core.getIDToken(this.audience); } @@ -63,24 +53,13 @@ export class LoginConfig { 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); - } - else { - throw new Error("Failed to fetch federated token."); - } + let [issuer, subjectClaim] = await jwtParser(this.federatedToken); + console.log("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim); } 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’"); + throw new Error("Unsupported value for environment is passed. The list of supported values for environment are 'azureusgovernment', 'azurechinacloud', 'azuregermancloud', 'azurecloud' or 'azurestack'"); } } } diff --git a/src/main.ts b/src/main.ts index 3848ed4dc..380c0222b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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,7 +15,7 @@ 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(); @@ -24,7 +23,6 @@ async function main() { // login to Azure Cli var cliLogin = new AzureCliLogin(loginConfig); await cliLogin.login(); - isAzCLISuccess = true; //login to Azure PowerShell if (loginConfig.enableAzPSSession) { @@ -37,7 +35,7 @@ async function main() { console.log("Login successful."); } catch (error) { - if (!isAzCLISuccess) { + if (!cliLogin.isSuccess) { 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 { From 0de1490d29ea92a72a3da74dd88cba4cdd201af9 Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Thu, 8 Jun 2023 14:37:09 +0800 Subject: [PATCH 2/8] update --- .github/workflows/azure-login-negative.yml | 4 +- .github/workflows/azure-login-positive.yml | 24 ++++++++++- README.md | 2 +- src/Cli/AzureCliLogin.ts | 50 ++++++++++++++++------ src/common/LoginConfig.ts | 12 +++--- src/main.ts | 4 +- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml index 2706180dc..d71cba100 100644 --- a/.github/workflows/azure-login-negative.yml +++ b/.github/workflows/azure-login-negative.yml @@ -146,7 +146,7 @@ jobs: tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true - enable-AzPSSession: true + # enable-AzPSSession: true - name: Run Azure Cli id: cli_3 @@ -373,4 +373,4 @@ 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.') diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml index 2a829db21..67d196ca3 100644 --- a/.github/workflows/azure-login-positive.yml +++ b/.github/workflows/azure-login-positive.yml @@ -96,6 +96,7 @@ jobs: npm install npm run build + # Attempt with secret will fail, Attempt with OIDC will succeed with SP2 - name: Login with both creds and individual parameters uses: ./ with: @@ -103,7 +104,8 @@ jobs: client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - enable-AzPSSession: true + allow-no-subscriptions: true + # enable-AzPSSession: true - name: Login with creds, disable ps session uses: ./ @@ -129,6 +131,24 @@ jobs: az group show --name GitHubActionGroup az vm list + - name: Login by OIDC with all info in creds + uses: ./ + with: + creds: ${{secrets.SP2}} + 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 creds, allow no subscription uses: ./ with: @@ -258,4 +278,4 @@ jobs: # with: # azPSVersion: "latest" # inlineScript: | - # Get-AzContext | Format-List \ No newline at end of file + # Get-AzContext | Format-List diff --git a/README.md b/README.md index b1daf66b5..3d408a1c1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Ac - To login using Managed Identities, follow [this](#configure-azure-managed-identities-with-self-hosted-runners) guidance. - The Action identifies the authentication method by the parameters you input. - 1. If the input is `creds` with all the parameters: `clientId`, `tenantId` and `clientSecret` are detected in your input, we will attempt to login by using service principal with the secret. + 1. If all the parameters: `clientId`, `tenantId` and `clientSecret` are detected in your input, we will attempt to login by using service principal with the secret. 2. If parameters: `clientId` and `tenantId` are detected in your input, we will attempt to login by using OIDC. 3. If parameters: `clientId` is detected in your input, we will attempt to login by using user-assigned managed identity. 4. If no parameter is detected in your input, we will attempt to login by using system-assigned managed identity. diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index 0d775c665..6ca82ae9d 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -94,9 +94,15 @@ export class AzureCliLogin { if (this.loginConfig.allowNoSubscriptionsLogin) { commonArgs.push("--allow-no-subscriptions"); } - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + try { + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + console.log('Az cli login succeed by using service principal with secret.'); + } + catch (error) { + core.error(`Failed with error: ${error}.\nStop login by using service principal with secret.`); + } } async loginWithOIDC() { @@ -113,9 +119,15 @@ export class AzureCliLogin { if (this.loginConfig.allowNoSubscriptionsLogin) { commonArgs.push("--allow-no-subscriptions"); } - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + try { + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + console.log('Az cli login succeed by using OIDC.'); + } + catch (error) { + core.error(`Failed with error: ${error}.\nStop login by using OIDC.`); + } } async loginWithUserManagedIdentity() { @@ -128,9 +140,15 @@ export class AzureCliLogin { if (this.loginConfig.allowNoSubscriptionsLogin) { commonArgs.push("--allow-no-subscriptions"); } - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + try { + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + console.log('Az cli login succeed by using user-assigned managed identity.'); + } + catch (error) { + core.error(`Failed with error: ${error}.\nStop login by using user-assigned managed identity.`); + } } async loginWithSystemManagedIdentity() { @@ -142,9 +160,15 @@ export class AzureCliLogin { if (this.loginConfig.allowNoSubscriptionsLogin) { commonArgs.push("--allow-no-subscriptions"); } - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + try { + await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + console.log('Az cli login succeed by using system-assigned managed identity.'); + } + catch (error) { + core.error(`Failed with error: ${error}.\nStop login by using system-assigned managed identity.`); + } } async setSubscription() { @@ -184,7 +208,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/common/LoginConfig.ts b/src/common/LoginConfig.ts index 7bda2389a..d579ecef6 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -33,11 +33,11 @@ export class LoginConfig { let creds = core.getInput('creds', { required: false }); let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null; if (creds) { - core.debug('using creds JSON...'); - this.servicePrincipalId = secrets.getSecret("$.clientId", true); - this.servicePrincipalKey = secrets.getSecret("$.clientSecret", true); - this.tenantId = secrets.getSecret("$.tenantId", true); - this.subscriptionId = secrets.getSecret("$.subscriptionId", true); + core.debug('Reading creds in JSON...'); + this.servicePrincipalId = this.servicePrincipalId ? this.servicePrincipalId : secrets.getSecret("$.clientId", false); + this.servicePrincipalKey = 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); } @@ -69,4 +69,4 @@ 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 380c0222b..5b241982e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,9 @@ async function main() { await spnlogin.login(); } - console.log("Login successful."); + if (!cliLogin.isSuccess) { + core.setFailed(`Az CLI Login failed. Please check the credentials and make sure az is installed on the runner.`); + } } catch (error) { if (!cliLogin.isSuccess) { From cd6dad51ee588903798c77855d3789153cf8670b Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Fri, 9 Jun 2023 17:37:05 +0800 Subject: [PATCH 3/8] update with commits --- .github/workflows/azure-login-negative.yml | 2 +- .github/workflows/azure-login-positive.yml | 41 ++++++++------ src/Cli/AzureCliLogin.ts | 63 ++++++++++------------ src/main.ts | 4 ++ 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml index d71cba100..10c921c95 100644 --- a/.github/workflows/azure-login-negative.yml +++ b/.github/workflows/azure-login-negative.yml @@ -146,7 +146,7 @@ jobs: tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true - # enable-AzPSSession: true + enable-AzPSSession: true - name: Run Azure Cli id: cli_3 diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml index 67d196ca3..e73f15115 100644 --- a/.github/workflows/azure-login-positive.yml +++ b/.github/workflows/azure-login-positive.yml @@ -62,18 +62,18 @@ jobs: tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true - # enable-AzPSSession: true + enable-AzPSSession: true - name: Run Azure Cli again run: | az account show - # - name: Run Azure PowerShell again - # uses: azure/powershell@v1.2.0 - # with: - # azPSVersion: "latest" - # inlineScript: | - # Get-AzContext | Format-List + - name: Run Azure PowerShell again + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List ParameterTest: strategy: @@ -105,7 +105,18 @@ jobs: tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} allow-no-subscriptions: true - # enable-AzPSSession: 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 creds, disable ps session uses: ./ @@ -136,18 +147,18 @@ jobs: with: creds: ${{secrets.SP2}} allow-no-subscriptions: true - # enable-AzPSSession: 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: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List - name: Login with creds, allow no subscription uses: ./ diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index 6ca82ae9d..d577aa7c6 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -82,22 +82,18 @@ export class AzureCliLogin { } async loginWithSecret() { - if (!(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId && this.loginConfig.servicePrincipalKey) || this.isSuccess) { + if (this.isSuccess || !(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId && this.loginConfig.servicePrincipalKey)) { + core.debug('Skip login with secret.'); return; } console.log('Attempting az cli login by using service principal with secret...\nNote: Azure/login action also supports OIDC login mechanism. If you want to use OIDC login, please do not input ClientSecret. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.'); - let commonArgs = ["--service-principal", - "-u", this.loginConfig.servicePrincipalId, + let args = ["--service-principal", + "--username", this.loginConfig.servicePrincipalId, "--tenant", this.loginConfig.tenantId, - "-p", this.loginConfig.servicePrincipalKey + `--password=${this.loginConfig.servicePrincipalKey}` ]; - if (this.loginConfig.allowNoSubscriptionsLogin) { - commonArgs.push("--allow-no-subscriptions"); - } try { - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + await this.callCliLogin(args); console.log('Az cli login succeed by using service principal with secret.'); } catch (error) { @@ -106,23 +102,19 @@ export class AzureCliLogin { } async loginWithOIDC() { - if (!(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId) || this.isSuccess) { + if (this.isSuccess || !(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId)) { + core.debug('Skip login with OIDC.'); return; } console.log('Attempting az cli login by using OIDC...'); await this.loginConfig.getFederatedToken(); - let commonArgs = ["--service-principal", - "-u", this.loginConfig.servicePrincipalId, + let args = ["--service-principal", + "--username", this.loginConfig.servicePrincipalId, "--tenant", this.loginConfig.tenantId, "--federated-token", this.loginConfig.federatedToken ]; - if (this.loginConfig.allowNoSubscriptionsLogin) { - commonArgs.push("--allow-no-subscriptions"); - } try { - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + await this.callCliLogin(args); console.log('Az cli login succeed by using OIDC.'); } catch (error) { @@ -131,19 +123,15 @@ export class AzureCliLogin { } async loginWithUserManagedIdentity() { - if (!this.loginConfig.servicePrincipalId || this.isSuccess) { + if (this.isSuccess || !this.loginConfig.servicePrincipalId) { + core.debug('Skip login with user assigned managed identity.'); return; } console.log('Attempting az cli login by using user-assigned managed identity...'); - let commonArgs = ["--identity", - "-u", this.loginConfig.servicePrincipalId]; - if (this.loginConfig.allowNoSubscriptionsLogin) { - commonArgs.push("--allow-no-subscriptions"); - } + let args = ["--identity", + "--username", this.loginConfig.servicePrincipalId]; try { - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + await this.callCliLogin(args); console.log('Az cli login succeed by using user-assigned managed identity.'); } catch (error) { @@ -153,17 +141,13 @@ export class AzureCliLogin { async loginWithSystemManagedIdentity() { if (this.isSuccess) { + core.debug('Skip login with system assigned managed identity.'); return; } console.log('Attempting az cli login by using system-assigned managed identity...'); - let commonArgs = ["--identity"]; - if (this.loginConfig.allowNoSubscriptionsLogin) { - commonArgs.push("--allow-no-subscriptions"); - } + let args = ["--identity"]; try { - await this.executeAzCliCommand('login', commonArgs, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; + await this.callCliLogin(args); console.log('Az cli login succeed by using system-assigned managed identity.'); } catch (error) { @@ -171,6 +155,15 @@ export class AzureCliLogin { } } + async callCliLogin(args: string[]) { + if (this.loginConfig.allowNoSubscriptionsLogin) { + args.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', args, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + } + async setSubscription() { if (this.loginConfig.allowNoSubscriptionsLogin) { return; diff --git a/src/main.ts b/src/main.ts index 5b241982e..d6ab569bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -27,6 +27,10 @@ async function main() { //login to Azure PowerShell if (loginConfig.enableAzPSSession) { console.log(`Running Azure PS Login`); + //remove the following 'if session' once the code is ready + if(!loginConfig.servicePrincipalKey){ + await loginConfig.getFederatedToken(); + } var spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(loginConfig); await spnlogin.initialize(); await spnlogin.login(); From 69b771e69b72a62f48aeae4408b3fac6b0ef45f0 Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Mon, 12 Jun 2023 14:29:12 +0800 Subject: [PATCH 4/8] update error handling --- src/Cli/AzureCliLogin.ts | 60 +++++++++++++++------------------------ src/common/LoginConfig.ts | 3 +- src/main.ts | 13 ++------- 3 files changed, 26 insertions(+), 50 deletions(-) diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index d577aa7c6..dbfe88244 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -44,6 +44,13 @@ export class AzureCliLogin { await this.loginWithOIDC(); await this.loginWithUserManagedIdentity(); await this.loginWithSystemManagedIdentity(); + + if (!this.isSuccess) { + throw new Error("Az CLI Login failed."); + } + else { + console.log("Az CLI Login succeeded."); + } } async setAzurestackEnvIfNecessary() { @@ -86,19 +93,12 @@ export class AzureCliLogin { core.debug('Skip login with secret.'); return; } - console.log('Attempting az cli login by using service principal with secret...\nNote: Azure/login action also supports OIDC login mechanism. If you want to use OIDC login, please do not input ClientSecret. Refer https://github.com/azure/login#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication for more details.'); let args = ["--service-principal", "--username", this.loginConfig.servicePrincipalId, "--tenant", this.loginConfig.tenantId, `--password=${this.loginConfig.servicePrincipalKey}` ]; - try { - await this.callCliLogin(args); - console.log('Az cli login succeed by using service principal with secret.'); - } - catch (error) { - core.error(`Failed with error: ${error}.\nStop login by using service principal with secret.`); - } + await this.callCliLogin(args, 'service principal with secret'); } async loginWithOIDC() { @@ -106,20 +106,13 @@ export class AzureCliLogin { core.debug('Skip login with OIDC.'); return; } - console.log('Attempting az cli login by using OIDC...'); await this.loginConfig.getFederatedToken(); let args = ["--service-principal", "--username", this.loginConfig.servicePrincipalId, "--tenant", this.loginConfig.tenantId, "--federated-token", this.loginConfig.federatedToken ]; - try { - await this.callCliLogin(args); - console.log('Az cli login succeed by using OIDC.'); - } - catch (error) { - core.error(`Failed with error: ${error}.\nStop login by using OIDC.`); - } + await this.callCliLogin(args, 'OIDC'); } async loginWithUserManagedIdentity() { @@ -127,16 +120,9 @@ export class AzureCliLogin { core.debug('Skip login with user assigned managed identity.'); return; } - console.log('Attempting az cli login by using user-assigned managed identity...'); let args = ["--identity", "--username", this.loginConfig.servicePrincipalId]; - try { - await this.callCliLogin(args); - console.log('Az cli login succeed by using user-assigned managed identity.'); - } - catch (error) { - core.error(`Failed with error: ${error}.\nStop login by using user-assigned managed identity.`); - } + await this.callCliLogin(args, 'user-assigned managed identity'); } async loginWithSystemManagedIdentity() { @@ -144,24 +130,24 @@ export class AzureCliLogin { core.debug('Skip login with system assigned managed identity.'); return; } - console.log('Attempting az cli login by using system-assigned managed identity...'); let args = ["--identity"]; + await this.callCliLogin(args, 'system-assigned managed identity'); + } + + async callCliLogin(args: string[], methodName: string) { try { - await this.callCliLogin(args); - console.log('Az cli login succeed by using system-assigned managed identity.'); + console.log(`Attempting az cli login by using ${methodName}...`); + if (this.loginConfig.allowNoSubscriptionsLogin) { + args.push("--allow-no-subscriptions"); + } + await this.executeAzCliCommand('login', args, true, this.loginOptions); + await this.setSubscription(); + this.isSuccess = true; + console.log(`Az cli login succeed by using ${methodName}.`); } catch (error) { - core.error(`Failed with error: ${error}.\nStop login by using system-assigned managed identity.`); - } - } - - async callCliLogin(args: string[]) { - if (this.loginConfig.allowNoSubscriptionsLogin) { - args.push("--allow-no-subscriptions"); + core.error(`Failed with error: ${error}.\nStop login by using ${methodName}.`); } - await this.executeAzCliCommand('login', args, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; } async setSubscription() { diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index d579ecef6..c0b8a0bf1 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -50,8 +50,7 @@ export class LoginConfig { this.federatedToken = await core.getIDToken(this.audience); } catch (error) { - core.error(`Please make sure to give write permissions to id-token in the workflow.`); - throw error; + core.error(`Failed with error: ${error}. Please make sure to give write permissions to id-token in the workflow.`); } let [issuer, subjectClaim] = await jwtParser(this.federatedToken); console.log("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim); diff --git a/src/main.ts b/src/main.ts index d6ab569bf..973620196 100644 --- a/src/main.ts +++ b/src/main.ts @@ -28,25 +28,16 @@ async function main() { if (loginConfig.enableAzPSSession) { console.log(`Running Azure PS Login`); //remove the following 'if session' once the code is ready - if(!loginConfig.servicePrincipalKey){ + if (!loginConfig.servicePrincipalKey) { await loginConfig.getFederatedToken(); } var spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(loginConfig); await spnlogin.initialize(); await spnlogin.login(); } - - if (!cliLogin.isSuccess) { - core.setFailed(`Az CLI Login failed. Please check the credentials and make sure az is installed on the runner.`); - } } catch (error) { - if (!cliLogin.isSuccess) { - 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}. 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`); } finally { // Reset AZURE_HTTP_USER_AGENT From 904c7f209a2d919c36df0f400c732a0ceaceb633 Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Thu, 29 Jun 2023 10:15:10 +0800 Subject: [PATCH 5/8] support auth-type --- .github/workflows/azure-login-negative.yml | 53 +++++++++ .github/workflows/azure-login-positive.yml | 56 +++++----- README.md | 38 ++++--- action.yml | 8 +- src/Cli/AzureCliLogin.ts | 119 +++++++++------------ src/common/LoginConfig.ts | 17 ++- src/main.ts | 6 +- 7 files changed, 179 insertions(+), 118 deletions(-) diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml index 10c921c95..cd2c57dcb 100644 --- a/.github/workflows/azure-login-negative.yml +++ b/.github/workflows/azure-login-negative.yml @@ -374,3 +374,56 @@ jobs: 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.') + + 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_13 + continue-on-error: true + uses: ./ + + - 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.') diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml index e73f15115..cd4a47f54 100644 --- a/.github/workflows/azure-login-positive.yml +++ b/.github/workflows/azure-login-positive.yml @@ -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 GitHubActionGroup + az vm list + + - name: Run Azure PowerShell + uses: azure/powershell@v1.2.0 + with: + azPSVersion: "latest" + inlineScript: | + Get-AzContext | Format-List + Get-AzResourceGroup -Name GitHubActionGroup + Get-AzVM + ParameterTest: strategy: matrix: @@ -96,28 +118,6 @@ jobs: npm install npm run build - # Attempt with secret will fail, Attempt with OIDC will succeed with SP2 - - 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 }} - 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 creds, disable ps session uses: ./ with: @@ -222,9 +222,10 @@ jobs: npm install npm run build - - name: Login with system assigned managed identity + - name: Login with system-assigned managed identity uses: ./ - # with: + with: + auth-type: IDENTITY # enable-AzPSSession: true - name: Run Azure Cli @@ -238,10 +239,11 @@ jobs: # inlineScript: | # Get-AzContext | Format-List - - name: Login with user assigned managed identity + - name: Login with user-assigned managed identity uses: ./ with: client-id: ${{ secrets.UMI1_CLIENT_ID }} + auth-type: IDENTITY # enable-AzPSSession: true - name: Run Azure Cli @@ -255,11 +257,12 @@ jobs: # inlineScript: | # Get-AzContext | Format-List - - name: Login with user assigned managed identity, subscription-id + - 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 @@ -278,6 +281,7 @@ jobs: with: client-id: ${{ secrets.UMI2_CLIENT_ID }} allow-no-subscriptions: true + auth-type: IDENTITY # enable-AzPSSession: true - name: Run Azure Cli diff --git a/README.md b/README.md index 3d408a1c1..60dfe2447 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ With [GitHub Actions for Azure](https://github.com/Azure/actions/), you can crea ## GitHub Action for Azure Login -With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Action, you can do an Azure login using [Azure Managed Identities and Azure service principal](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#managed-identity-types) to run Az CLI and Azure PowerShell scripts. +With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Action, you can do an Azure login using [Azure Managed Identities](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#managed-identity-types) and [Azure service principal](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals) to run Azure CLI and Azure PowerShell scripts. - By default, the action only logs in with the Azure CLI (using the `az login` command). To log in with the Az PowerShell module, set `enable-AzPSSession` to true. To login to Azure tenants without any subscriptions, set the optional parameter `allow-no-subscriptions` to true. @@ -28,18 +28,18 @@ With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Ac 3. Within the Job deploying to Azure, add Azure/login action and pass the `client-id` and `tenant-id` of the Azure Managed Identity/service principal associated with an OIDC Federated Identity Credential created in step (i). You also need to pass `subscription-id` or set `allow-no-subscriptions` to true. - To login using Managed Identities, follow [this](#configure-azure-managed-identities-with-self-hosted-runners) guidance. -- The Action identifies the authentication method by the parameters you input. - 1. If all the parameters: `clientId`, `tenantId` and `clientSecret` are detected in your input, we will attempt to login by using service principal with the secret. - 2. If parameters: `clientId` and `tenantId` are detected in your input, we will attempt to login by using OIDC. - 3. If parameters: `clientId` is detected in your input, we will attempt to login by using user-assigned managed identity. - 4. If no parameter is detected in your input, we will attempt to login by using system-assigned managed identity. +- The Action provides a parameter `auth-type` to identify the type of authentication. + 1. If `auth-type: SERVICE_PRINCIPAL` with `clientId`, `tenantId` and `clientSecret` detected in your input, we will attempt to login by using service principal with the secret. + 2. If `auth-type: SERVICE_PRINCIPAL` with `clientId` and `tenantId` detected in your input, we will attempt to login by using OIDC. + 3. If `auth-type: IDENTITY` with `clientId` detected in your input, we will attempt to login by using user-assigned managed identity. + 4. If `auth-type: IDENTITY` without `clientId` detected in your input, we will attempt to login by using system-assigned managed identity. Note: - Ensure the CLI version is 2.30 or above to use OIDC support. - By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by AD App (Service Principal) is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hrs. This expiration time is further configurable in Azure. Refger to [access-token lifetime](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details. -## Sample workflow that uses Azure login action to run az cli +## Sample workflow that uses Azure login action to run Azure CLI ```yaml # File: .github/workflows/workflow.yml @@ -89,7 +89,7 @@ jobs: ``` -## Sample workflow that uses Azure login action using OIDC to run az cli (Linux) +## Sample workflow that uses Azure login action using OIDC to run azure cli (Linux) ```yaml # File: .github/workflows/OIDC_workflow.yml @@ -104,7 +104,7 @@ jobs: build-and-deploy: runs-on: ubuntu-latest steps: - - name: Az CLI login + - name: Azure CLI login uses: azure/login@v1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -168,6 +168,8 @@ jobs: steps: - name: Azure Login uses: azure/login@v1 + with: + auth-type: IDENTITY - name: Show group list run: | @@ -189,6 +191,8 @@ jobs: with: client-id: ${{ secrets.AZURE_CLIENT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + auth-type: IDENTITY + - name: Show group list run: | az group list @@ -215,7 +219,7 @@ jobs: Refer to the [Azure PowerShell](https://github.com/azure/powershell) GitHub Action to run your Azure PowerShell scripts. -## Sample Azure Login workflow that uses Azure login action to run az cli on Azure Stack Hub +## Sample Azure Login workflow that uses Azure login action to run Azure CLI on Azure Stack Hub ```yaml # File: .github/workflows/workflow.yml @@ -247,14 +251,14 @@ Refer to the [Azure Stack Hub Login Action Tutorial](https://learn.microsoft.com For using any credentials like Azure Service Principal, Publish Profile etc add them as [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the GitHub repository and then use them in the workflow. -Follow the following steps to configure Azure Service Principal with a secret in the scope of `resource-group` as the role of `contributor`. +Follow the following steps to configure Azure Service Principal with a secret in the scope of `resource-group` as the role of `reader`. - Define a new secret under your repository settings, Add secret menu -- Store the output of the below [az cli](https://learn.microsoft.com/cli/azure/?view=azure-cli-latest) command as the value of secret variable, for example 'AZURE_CREDENTIALS' +- Store the output of the below [Azure CLI](https://learn.microsoft.com/cli/azure/?view=azure-cli-latest) command as the value of secret variable, for example 'AZURE_CREDENTIALS' ```bash - az ad sp create-for-rbac --name "myApp" --role contributor \ + az ad sp create-for-rbac --name "myApp" --role reader \ --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \ --sdk-auth ``` @@ -293,13 +297,13 @@ If you already created and assigned a Service Principal in Azure you can manuall ### Configure a Federated Credential to use OIDC based authentication -Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a federated identity credential on user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider (preview) which has more details about the Azure Workload Identity Federation (OIDC) support. +Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a federated identity credential on user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider which has more details about the Azure Workload Identity Federation (OIDC) support. You can add federated credentials in the Azure portal or with the Microsoft Graph REST API. ### Configure Azure Managed Identities with self-hosted runners -If you want to use managed identities (system- or user-assigned) to sign in, a self-hosted Github runner is required to be installed on an Azure VM with a managed identity configured. Please refer to Microsoft's documentation at ["Configure managed identities for Azure resources on an Azure VM"](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-cli-windows-vm). To use system-assigned managed identity, there are no mandatory parameters. To use user-assigned managed identity, `client-id` of the identity is mandatory. +If you want to use (system- or user-assigned) managed identities to sign in, a self-hosted Github runner is required to be installed on an Azure VM with a managed identity configured. Please refer to Microsoft's documentation at ["Configure managed identities for Azure resources on an Azure VM"](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-cli-windows-vm). To use managed identity, `auth-type` must be set to `IDENTITY`. To use user-assigned managed identity, `client-id` of the identity is mandatory. To get the `client-id` of a user-assigned managed identity, use the command: ```bash @@ -343,9 +347,9 @@ This action doesn't implement ```az logout``` by default at the end of execution az account clear ``` -## Az CLI dependency +## Azure CLI dependency -Internally in this action, we use azure CLI and execute `az login` with the credentials provided through secrets. In order to validate the new az CLI releases for this action, [canary test workflow](.github/workflows/azure-login-canary.yml) is written which will execute the action on [az CLI's edge build](https://github.com/Azure/azure-cli#edge-builds) which will fail incase of any breaking change is being introduced in the new upcoming release. The test results can be posted on a slack or teams channel using the corresponding integrations. Incase of a failure, the concern will be raised to [azure-cli](https://github.com/Azure/azure-cli) for taking a necessary action and also the latest CLI installation will be postponed in [Runner VMs](https://github.com/actions/virtual-environments) as well for hosted runner to prevent the workflows failing due to the new CLI changes. +Internally in this action, we use azure CLI and execute `az login` with the credentials provided through secrets. In order to validate the new Azure CLI releases for this action, [canary test workflow](.github/workflows/azure-login-canary.yml) is written which will execute the action on [Azure CLI's edge build](https://github.com/Azure/azure-cli#edge-builds) which will fail incase of any breaking change is being introduced in the new upcoming release. The test results can be posted on a slack or teams channel using the corresponding integrations. Incase of a failure, the concern will be raised to [azure-cli](https://github.com/Azure/azure-cli) for taking a necessary action and also the latest CLI installation will be postponed in [Runner VMs](https://github.com/actions/virtual-environments) as well for hosted runner to prevent the workflows failing due to the new CLI changes. ## Contributing diff --git a/action.yml b/action.yml index c9883bb8b..02b0c9edf 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 using OIDC and run your Azure CLI or Azure PowerShell based actions or scripts. github.com/Azure/Actions' 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 dbfe88244..f702d4d1e 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -8,20 +8,19 @@ export class AzureCliLogin { loginConfig: LoginConfig; azPath: string; loginOptions: ExecOptions; - isSuccess: boolean; constructor(loginConfig: LoginConfig) { this.loginConfig = loginConfig; this.loginOptions = defaultExecOptions(); - this.isSuccess = false; } async login() { + console.log(`Running Azure CLI Login`); this.azPath = await io.which("az", true); if (!this.azPath) { - throw new Error("az cli is not found in the runner."); + throw new Error("Azure CLI is not found in the runner."); } - core.debug(`az cli path: ${this.azPath}`); + core.debug(`Azure CLI path: ${this.azPath}`); let output: string = ""; const execOptions: any = { @@ -32,24 +31,34 @@ 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); + await this.executeAzCliCommand(["cloud", "set", "-n", this.loginConfig.environment], false); console.log(`Done setting cloud: "${this.loginConfig.environment}"`); - await this.loginWithSecret(); - await this.loginWithOIDC(); - await this.loginWithUserManagedIdentity(); - await this.loginWithSystemManagedIdentity(); - - if (!this.isSuccess) { - throw new Error("Az CLI Login failed."); + if (this.loginConfig.authType == "service_principal") { + let args = ["--service-principal", + "--username", this.loginConfig.servicePrincipalId, + "--tenant", this.loginConfig.tenantId + ]; + if (this.loginConfig.servicePrincipalKey) { + await this.loginWithSecret(args); + } + else { + await this.loginWithOIDC(args); + } } else { - console.log("Az CLI Login succeeded."); + let args = ["--identity"]; + if (this.loginConfig.servicePrincipalId) { + await this.loginWithUserAssignedIdentity(args); + } + else { + await this.loginWithSystemAssignedIdentity(args); + } } } @@ -63,8 +72,8 @@ export class AzureCliLogin { console.log(`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}"`); @@ -79,75 +88,46 @@ 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}"`) } - async loginWithSecret() { - if (this.isSuccess || !(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId && this.loginConfig.servicePrincipalKey)) { - core.debug('Skip login with secret.'); - return; - } - let args = ["--service-principal", - "--username", this.loginConfig.servicePrincipalId, - "--tenant", this.loginConfig.tenantId, - `--password=${this.loginConfig.servicePrincipalKey}` - ]; + async loginWithSecret(args: string[]) { + 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.") + args.push(`--password=${this.loginConfig.servicePrincipalKey}`); await this.callCliLogin(args, 'service principal with secret'); } - async loginWithOIDC() { - if (this.isSuccess || !(this.loginConfig.servicePrincipalId && this.loginConfig.tenantId)) { - core.debug('Skip login with OIDC.'); - return; - } + async loginWithOIDC(args: string[]) { await this.loginConfig.getFederatedToken(); - let args = ["--service-principal", - "--username", this.loginConfig.servicePrincipalId, - "--tenant", this.loginConfig.tenantId, - "--federated-token", this.loginConfig.federatedToken - ]; + args.push("--federated-token", this.loginConfig.federatedToken); await this.callCliLogin(args, 'OIDC'); } - async loginWithUserManagedIdentity() { - if (this.isSuccess || !this.loginConfig.servicePrincipalId) { - core.debug('Skip login with user assigned managed identity.'); - return; - } - let args = ["--identity", - "--username", this.loginConfig.servicePrincipalId]; + async loginWithUserAssignedIdentity(args: string[]) { + args.push("--username", this.loginConfig.servicePrincipalId); await this.callCliLogin(args, 'user-assigned managed identity'); } - async loginWithSystemManagedIdentity() { - if (this.isSuccess) { - core.debug('Skip login with system assigned managed identity.'); - return; - } - let args = ["--identity"]; + async loginWithSystemAssignedIdentity(args: string[]) { await this.callCliLogin(args, 'system-assigned managed identity'); } async callCliLogin(args: string[], methodName: string) { - try { - console.log(`Attempting az cli login by using ${methodName}...`); - if (this.loginConfig.allowNoSubscriptionsLogin) { - args.push("--allow-no-subscriptions"); - } - await this.executeAzCliCommand('login', args, true, this.loginOptions); - await this.setSubscription(); - this.isSuccess = true; - console.log(`Az cli login succeed by using ${methodName}.`); - } - catch (error) { - core.error(`Failed with error: ${error}.\nStop login by using ${methodName}.`); + console.log(`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(); + console.log(`Azure CLI login succeed by using ${methodName}.`); } async setSubscription() { @@ -155,21 +135,20 @@ export class AzureCliLogin { return; } if (!this.loginConfig.subscriptionId) { - core.warning('No subscription-id is given. Skip setting subscription...If there are mutiple subscriptions under the tenant, please input subscription-id to specify which subscription to use.'); + core.warning('No subscription-id is given. Skip setting subscription... If there are mutiple subscriptions under the tenant, please input subscription-id to specify which subscription to use.'); return; } - let args = ["--subscription", this.loginConfig.subscriptionId]; - await this.executeAzCliCommand('account set', args, true, this.loginOptions); - console.log('Subscription is set successfully.'); + let args = ["account", "set", "--subscription", this.loginConfig.subscriptionId]; + await this.executeAzCliCommand(args, true, this.loginOptions); + console.log("Subscription is set successfully."); } async executeAzCliCommand( - command: string, args: string[], silent?: boolean, execOptions: any = {}) { execOptions.silent = !!silent; - await exec.exec(`"${this.azPath}" ${command}`, args, execOptions); + await exec.exec(`"${this.azPath}"`, args, execOptions); } } diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index c0b8a0bf1..1f52dcf56 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -9,6 +9,11 @@ export class LoginConfig { "azurecloud", "azurestack"]); + static readonly azureSupportedAuthType = new Set([ + "service_principal", + "identity"]); + + authType: string; servicePrincipalId: string; servicePrincipalKey: string; tenantId: string; @@ -24,6 +29,7 @@ export class LoginConfig { 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').toLowerCase(); this.servicePrincipalId = core.getInput('client-id', { required: false }); this.servicePrincipalKey = null; @@ -50,7 +56,8 @@ export class LoginConfig { this.federatedToken = await core.getIDToken(this.audience); } catch (error) { - core.error(`Failed with error: ${error}. Please make sure to give write permissions to id-token in the workflow.`); + core.error(`Please make sure to give write permissions to id-token in the workflow.`); + throw error; } let [issuer, subjectClaim] = await jwtParser(this.federatedToken); console.log("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim); @@ -60,6 +67,14 @@ export class LoginConfig { 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'"); } + if (!LoginConfig.azureSupportedAuthType.has(this.authType)) { + throw new Error("Unsupported value for authentication type is passed. The list of supported values for auth-type are 'SERVICE_PRINCIPAL' or 'IDENTITY'"); + } + if (this.authType == "service_principal") { + if (!this.servicePrincipalId || !this.tenantId) { + throw new Error("Using auth-type: SERVICE_PRINCIPAL. Not all values are present in the credentials. Ensure clientId and tenantId are supplied."); + } + } } } diff --git a/src/main.ts b/src/main.ts index 973620196..0d07be174 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,7 +20,7 @@ async function main() { await loginConfig.initialize(); await loginConfig.validate(); - // login to Azure Cli + // login to Azure CLI var cliLogin = new AzureCliLogin(loginConfig); await cliLogin.login(); @@ -35,9 +35,11 @@ async function main() { await spnlogin.initialize(); await spnlogin.login(); } + console.log("Login successful."); } catch (error) { - core.setFailed(`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}. Please check the credentials and auth-type, and make sure 'az' is installed on the runner. For more information refer https://github.com/Azure/login#readme.`); + core.debug(error.stack); } finally { // Reset AZURE_HTTP_USER_AGENT From 865e9b6b312252116f79b704219f57c3b1b4dd1e Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Tue, 4 Jul 2023 16:26:23 +0800 Subject: [PATCH 6/8] update default value to uppercase --- README.md | 2 +- action.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 60dfe2447..abb4834ec 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Ac 3. Within the Job deploying to Azure, add Azure/login action and pass the `client-id` and `tenant-id` of the Azure Managed Identity/service principal associated with an OIDC Federated Identity Credential created in step (i). You also need to pass `subscription-id` or set `allow-no-subscriptions` to true. - To login using Managed Identities, follow [this](#configure-azure-managed-identities-with-self-hosted-runners) guidance. -- The Action provides a parameter `auth-type` to identify the type of authentication. +- The Action provides a parameter `auth-type` with value list `[SERVICE_PRINCIPAL, IDENTITY]` to identify the type of authentication. 1. If `auth-type: SERVICE_PRINCIPAL` with `clientId`, `tenantId` and `clientSecret` detected in your input, we will attempt to login by using service principal with the secret. 2. If `auth-type: SERVICE_PRINCIPAL` with `clientId` and `tenantId` detected in your input, we will attempt to login by using OIDC. 3. If `auth-type: IDENTITY` with `clientId` detected in your input, we will attempt to login by using user-assigned managed identity. diff --git a/action.yml b/action.yml index 02b0c9edf..e3eea6e71 100644 --- a/action.yml +++ b/action.yml @@ -31,9 +31,9 @@ inputs: required: false default: 'api://AzureADTokenExchange' auth-type: - description: 'The type of authentication. Supported values are service-principal, identity. Default value is service-principal' + description: 'The type of authentication. Supported values are SERVICE_PRINCIPAL, IDENTITY. Default value is SERVICE_PRINCIPAL' required: false - default: 'service_principal' + default: 'SERVICE_PRINCIPAL' branding: icon: 'login.svg' color: 'blue' From a9b4236d86a971161aa5d4ccc8cd1a82d62f42bd Mon Sep 17 00:00:00 2001 From: shiyingchen Date: Mon, 31 Jul 2023 14:00:40 +0800 Subject: [PATCH 7/8] add warning --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index abb4834ec..433f8fb96 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,9 @@ With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Ac 3. If `auth-type: IDENTITY` with `clientId` detected in your input, we will attempt to login by using user-assigned managed identity. 4. If `auth-type: IDENTITY` without `clientId` detected in your input, we will attempt to login by using system-assigned managed identity. -Note: - -- Ensure the CLI version is 2.30 or above to use OIDC support. -- By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by AD App (Service Principal) is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hrs. This expiration time is further configurable in Azure. Refger to [access-token lifetime](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details. +> **Note** +> - Ensure the CLI version is 2.30 or above to use OIDC support. +> - By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by AD App (Service Principal) is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hrs. This expiration time is further configurable in Azure. Refer to [access-token lifetime](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details. ## Sample workflow that uses Azure login action to run Azure CLI @@ -310,6 +309,9 @@ To get the `client-id` of a user-assigned managed identity, use the command: az vm identity show --resource-group --name --query userAssignedIdentities ``` +> **Warning** +> Avoid using managed identity login on self-hosted runners in public repositories. Managed identities enable secure authentication with Azure resources and obtain Azure AD tokens without the need for explicit credential management. Any user can open pull requests against your repository and access your self-hosted runners without credentials. See more details in [self-hosted runner security](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security). + ## Support for using `allow-no-subscriptions` flag with az login Capability has been added to support access to tenants without subscriptions. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default. From f05af2a0bcfb98f1b774ba803caeb8d39c70f3cd Mon Sep 17 00:00:00 2001 From: MoChilia Date: Tue, 15 Aug 2023 15:17:17 +0800 Subject: [PATCH 8/8] revert changes in readme --- README.md | 104 ++++++++++-------------------------------------------- 1 file changed, 19 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 433f8fb96..e6e349142 100644 --- a/README.md +++ b/README.md @@ -8,37 +8,24 @@ With [GitHub Actions for Azure](https://github.com/Azure/actions/), you can crea ## GitHub Action for Azure Login -With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Action, you can do an Azure login using [Azure Managed Identities](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#managed-identity-types) and [Azure service principal](https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals) to run Azure CLI and Azure PowerShell scripts. +With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Action, you can do an Azure login using [Azure Managed Identities and Azure service principal](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#managed-identity-types) to run Az CLI and Azure PowerShell scripts. - By default, the action only logs in with the Azure CLI (using the `az login` command). To log in with the Az PowerShell module, set `enable-AzPSSession` to true. To login to Azure tenants without any subscriptions, set the optional parameter `allow-no-subscriptions` to true. -- About the parameter `subscription-id`: This parameter is used to specify which subscription to work. If you don't specify a subscription, the Action uses your current, active subscription. - - To login into one of the Azure Government clouds or Azure Stack, set the optional parameter `environment` with one of the supported values `AzureUSGovernment` or `AzureChinaCloud` or `AzureStack`. If this parameter is not specified, it takes the default value `AzureCloud` and connects to the Azure Public Cloud. Additionally, the parameter `creds` takes the Azure service principal created in the particular cloud to connect (Refer to the [Configure a service principal with a secret](#configure-a-service-principal-with-a-secret) section below for details). - -- The Action supports three different ways of authentication with Azure. - 1. Using the Azure Service Principal with secrets. - 2. Using OpenID connect (OIDC) method of authentication by Azure [Workload Identity Federation](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation). **We recommend using OIDC based authentication for increased security.** - 3. Using the Managed Identity configued on an Azure VM. - +- The Action supports two different ways of authentication with Azure. One using the Azure Service Principal with secrets. The other is OpenID connect (OIDC) method of authentication using Azure [Workload Identity Federation](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation). **We recommend using OIDC based authentication for increased security.** - To login using Azure Service Principal with a secret, follow [this](#configure-a-service-principal-with-a-secret) guidance. - To login using **OpenID Connect (OIDC) based Federated Identity Credentials**, you need to first configure trust between GitHub workflow and an Azure Managed Identity or an Azure AD App (Service Principal) 1. Follow [this](#configure-a-federated-credential-to-use-oidc-based-authentication) guidance to create a Federated Credential associated with your Azure Managed Identity or AD App (Service Principal). This is needed to establish OIDC trust between GitHub deployment workflows and the specific Azure resources scoped by the Managed Identity/service principal. 2. In your GitHub workflow, Set `permissions:` with `id-token: write` at workflow level or job level based on whether the OIDC token needs to be auto-generated for all Jobs or a specific Job. 3. Within the Job deploying to Azure, add Azure/login action and pass the `client-id` and `tenant-id` of the Azure Managed Identity/service principal associated with an OIDC Federated Identity Credential created in step (i). You also need to pass `subscription-id` or set `allow-no-subscriptions` to true. -- To login using Managed Identities, follow [this](#configure-azure-managed-identities-with-self-hosted-runners) guidance. -- The Action provides a parameter `auth-type` with value list `[SERVICE_PRINCIPAL, IDENTITY]` to identify the type of authentication. - 1. If `auth-type: SERVICE_PRINCIPAL` with `clientId`, `tenantId` and `clientSecret` detected in your input, we will attempt to login by using service principal with the secret. - 2. If `auth-type: SERVICE_PRINCIPAL` with `clientId` and `tenantId` detected in your input, we will attempt to login by using OIDC. - 3. If `auth-type: IDENTITY` with `clientId` detected in your input, we will attempt to login by using user-assigned managed identity. - 4. If `auth-type: IDENTITY` without `clientId` detected in your input, we will attempt to login by using system-assigned managed identity. +Note: -> **Note** -> - Ensure the CLI version is 2.30 or above to use OIDC support. -> - By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by AD App (Service Principal) is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hrs. This expiration time is further configurable in Azure. Refer to [access-token lifetime](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details. +- Ensure the CLI version is 2.30 or above to use OIDC support. +- By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by AD App (Service Principal) is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hrs. This expiration time is further configurable in Azure. Refer to [access-token lifetime](https://learn.microsoft.com/en-us/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details. -## Sample workflow that uses Azure login action to run Azure CLI +## Sample workflow that uses Azure login action to run az cli ```yaml # File: .github/workflows/workflow.yml @@ -88,7 +75,7 @@ jobs: ``` -## Sample workflow that uses Azure login action using OIDC to run azure cli (Linux) +## Sample workflow that uses Azure login action using OIDC to run az cli (Linux) ```yaml # File: .github/workflows/OIDC_workflow.yml @@ -103,7 +90,7 @@ jobs: build-and-deploy: runs-on: ubuntu-latest steps: - - name: Azure CLI login + - name: Az CLI login uses: azure/login@v1 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -115,6 +102,7 @@ jobs: with: azcliversion: latest inlineScript: | + az account show az group list ``` @@ -155,48 +143,6 @@ jobs: Refer to the [Azure PowerShell](https://github.com/azure/powershell) GitHub Action to run your Azure PowerShell scripts. -## Sample workflow that uses Azure login action with system-assigned managed identity - -```yaml -# File: .github/workflows/workflow.yml -on: [push] -name: Azure System-assigned Managed Identity sample -jobs: - build-and-deploy: - runs-on: self-hosted - steps: - - name: Azure Login - uses: azure/login@v1 - with: - auth-type: IDENTITY - - - name: Show group list - run: | - az group list -``` - -## Sample workflow that uses Azure login action with user-assigned managed identity - -```yaml -# File: .github/workflows/workflow.yml -on: [push] -name: Azure User-assigned Managed Identity sample -jobs: - build-and-deploy: - runs-on: self-hosted - steps: - - name: Azure Login - uses: azure/login@v1 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - auth-type: IDENTITY - - - name: Show group list - run: | - az group list -``` - ## Sample to connect to Azure US Government cloud ```yaml @@ -218,7 +164,7 @@ jobs: Refer to the [Azure PowerShell](https://github.com/azure/powershell) GitHub Action to run your Azure PowerShell scripts. -## Sample Azure Login workflow that uses Azure login action to run Azure CLI on Azure Stack Hub +## Sample Azure Login workflow that uses Azure login action to run az cli on Azure Stack Hub ```yaml # File: .github/workflows/workflow.yml @@ -250,19 +196,19 @@ Refer to the [Azure Stack Hub Login Action Tutorial](https://learn.microsoft.com For using any credentials like Azure Service Principal, Publish Profile etc add them as [secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the GitHub repository and then use them in the workflow. -Follow the following steps to configure Azure Service Principal with a secret in the scope of `resource-group` as the role of `reader`. +Follow the following steps to configure Azure Service Principal with a secret at the scope of `{resource-group}` as the role of `contributor`: - Define a new secret under your repository settings, Add secret menu - Store the output of the below [Azure CLI](https://learn.microsoft.com/cli/azure/?view=azure-cli-latest) command as the value of secret variable, for example 'AZURE_CREDENTIALS' ```bash - az ad sp create-for-rbac --name "myApp" --role reader \ + az ad sp create-for-rbac --name "myApp" --role contributor \ --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \ - --sdk-auth + --json-auth ``` -Please assign the service principal with proper `role` and `scope` you desired. Replace `{subscription-id}` and `{resource-group}` with the subscription and resource group details, respectively. +Replace `{subscription-id}` and `{resource-group}` with the subscription and resource group details, respectively. The command should output a JSON object similar to this: @@ -279,7 +225,7 @@ The command should output a JSON object similar to this: ``` - Now in the workflow file in your branch: `.github/workflows/workflow.yml` replace the secret in Azure login action with your secret (Refer to the example above) -- Note: The above `az ad sp create-for-rbac` command will give you the `--sdk-auth` deprecation warning. As we are working with CLI for this deprecation process, we strongly recommend users to use this `--sdk-auth` flag as the result dictionary output changes and not accepted by login action if `--sdk-auth` is not used. +- Note: Starting from Azure CLI versions 2.51.0, the argument `--json-auth` outputs the result dictionary accepted by the login action. - If you want to pass Subscription ID, Tenant ID, Client ID, and Client Secret as individual parameters instead of bundling them in a single JSON object (creds) to address the [security concerns](https://docs.github.com/actions/security-guides/encrypted-secrets) for Non-OIDC login, below snippet can help with the same. ```yaml @@ -296,25 +242,13 @@ If you already created and assigned a Service Principal in Azure you can manuall ### Configure a Federated Credential to use OIDC based authentication -Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a federated identity credential on user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider which has more details about the Azure Workload Identity Federation (OIDC) support. +Please refer to Microsoft's documentation at ["Configure a federated identity credential on an app”](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) and ["Configure a user-assigned managed identity"](https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-user-assigned-managed-identity?pivots=identity-wif-mi-methods-azp#github-actions-deploying-azure-resources) to trust an external identity provider (preview) which has more details about the Azure Workload Identity Federation (OIDC) support. You can add federated credentials in the Azure portal or with the Microsoft Graph REST API. -### Configure Azure Managed Identities with self-hosted runners - -If you want to use (system- or user-assigned) managed identities to sign in, a self-hosted Github runner is required to be installed on an Azure VM with a managed identity configured. Please refer to Microsoft's documentation at ["Configure managed identities for Azure resources on an Azure VM"](https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-cli-windows-vm). To use managed identity, `auth-type` must be set to `IDENTITY`. To use user-assigned managed identity, `client-id` of the identity is mandatory. -To get the `client-id` of a user-assigned managed identity, use the command: - -```bash -az vm identity show --resource-group --name --query userAssignedIdentities -``` - -> **Warning** -> Avoid using managed identity login on self-hosted runners in public repositories. Managed identities enable secure authentication with Azure resources and obtain Azure AD tokens without the need for explicit credential management. Any user can open pull requests against your repository and access your self-hosted runners without credentials. See more details in [self-hosted runner security](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security). - ## Support for using `allow-no-subscriptions` flag with az login -Capability has been added to support access to tenants without subscriptions. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default. +Capability has been added to support access to tenants without subscriptions for both OIDC and non-OIDC. This can be useful to run tenant level commands, such as `az ad`. The action accepts an optional parameter `allow-no-subscriptions` which is `false` by default. ```yaml # File: .github/workflows/workflow.yml @@ -349,9 +283,9 @@ This action doesn't implement ```az logout``` by default at the end of execution az account clear ``` -## Azure CLI dependency +## Az CLI dependency -Internally in this action, we use azure CLI and execute `az login` with the credentials provided through secrets. In order to validate the new Azure CLI releases for this action, [canary test workflow](.github/workflows/azure-login-canary.yml) is written which will execute the action on [Azure CLI's edge build](https://github.com/Azure/azure-cli#edge-builds) which will fail incase of any breaking change is being introduced in the new upcoming release. The test results can be posted on a slack or teams channel using the corresponding integrations. Incase of a failure, the concern will be raised to [azure-cli](https://github.com/Azure/azure-cli) for taking a necessary action and also the latest CLI installation will be postponed in [Runner VMs](https://github.com/actions/virtual-environments) as well for hosted runner to prevent the workflows failing due to the new CLI changes. +Internally in this action, we use azure CLI and execute `az login` with the credentials provided through secrets. In order to validate the new az CLI releases for this action, [canary test workflow](.github/workflows/azure-login-canary.yml) is written which will execute the action on [az CLI's edge build](https://github.com/Azure/azure-cli#edge-builds) which will fail incase of any breaking change is being introduced in the new upcoming release. The test results can be posted on a slack or teams channel using the corresponding integrations. Incase of a failure, the concern will be raised to [azure-cli](https://github.com/Azure/azure-cli) for taking a necessary action and also the latest CLI installation will be postponed in [Runner VMs](https://github.com/actions/virtual-environments) as well for hosted runner to prevent the workflows failing due to the new CLI changes. ## Contributing