Compare commits

..

1 Commits

Author SHA1 Message Date
Balaga Gayatri
dfb20b2efa Rename bug-report---feature-request.md to bug-report-feature-request.md 2021-08-09 12:57:02 +05:30
20 changed files with 1979 additions and 9068 deletions

21
.github/workflows/auto-triage-issues vendored Normal file
View File

@@ -0,0 +1,21 @@
name: "Auto-Labelling Issues"
on:
issues:
types: [opened, edited]
jobs:
auto_label:
runs-on: ubuntu-latest
name: Auto-Labelling Issues
steps:
- name: Label Step
uses: larrylawl/Auto-Github-Issue-Labeller@v1.0
with:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
REPOSITORY: ${{github.repository}}
DELTA: "7"
CONFIDENCE: "2"
FEATURE: "enhancement"
BUG: "bug"
DOCS: "documentation"

View File

@@ -1,94 +0,0 @@
#This workflow is used to test azure login action for CLI edge build. Visit, https://github.com/Azure/azure-cli#edge-builds for more details.
name: Run Azure Login Canary Test
on:
workflow_dispatch:
schedule:
- cron: ' 0 8 * * *'
permissions:
id-token: write
contents: read
jobs:
az-login-test:
runs-on: ubuntu-latest
steps:
- name : Check Az version before installing
run: az --version
- name: Installing Az CLI Edge build
run: |
cd ../..
CWD="$(pwd)"
python3 -m venv canary-venv
. canary-venv/bin/activate
echo "***********activated virual environment**********"
python3 -m pip install --upgrade pip
echo "***************started installing cli edge build******************"
pip3 install -q --upgrade --pre azure-cli --extra-index-url https://azurecliprod.blob.core.windows.net/edge --no-cache-dir --upgrade-strategy=eager
echo "***************installed cli Edge build*******************"
echo "$CWD/canary-venv/bin" >> $GITHUB_PATH
az --version
- name: Check out repository
uses: actions/checkout@v3
- name: 'Az CLI login with subscription'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az account show
- name: 'Az CLI login without subscription'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
allow-no-subscriptions: true
- run: |
az account show
- name: 'Az CLI login with subscription OIDC'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }}
- run: |
az account show
- name: 'Az CLI login without subscription OIDC'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
allow-no-subscriptions: true
- run: |
az account show
slack-post-result:
runs-on: ubuntu-latest
# continue-on-error: true
if: ${{ always() }}
needs: [az-login-test]
steps:
- name: Create slack post
id: slack_report
run: |
TITLE="Login action canary tests update - "
DATEVAR=`date "+%d/%m/%YT%H:%M:%S"`
TITLE="${TITLE}${DATEVAR}"
REPORT="${TITLE}\r\nLink to run - https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\r\n"
RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
REPORT="${REPORT}\r\n"
if [ ${{needs.az-login-test.result}} == 'success' ]; then REPORT="${REPORT}\r\n|✅|<${RUN_URL}|az-login-test>"; else REPORT="${REPORT}\r\n|❌|<${RUN_URL}|az-login-test>"; fi
echo "::set-output name=report::$REPORT"
- name: Post to slack
shell: bash
run: curl -X POST -H 'Content-type:application/json' --data '{"blocks":[{"type":"section","text":{"type":"mrkdwn","text":"${{steps.slack_report.outputs.report}}"}}]}' https://hooks.slack.com/services/${{SECRETS.SLACK_CHANNEL_SECRET}}

View File

@@ -1,129 +0,0 @@
name: Run Azure Login Integration Tests
on:
workflow_dispatch:
schedule:
- cron: '0 */3 * * *'
permissions:
id-token: write
contents: write
jobs:
az-login-test-non-oidc:
runs-on: ubuntu-latest
# continue-on-error: true
steps:
- name: 'Az CLI login with subscription'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az account show
# az webapp list
- name: 'Az CLI login without subscription'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
allow-no-subscriptions: true
- run: |
az account show
- name: 'Azure PowerShell login with subscription'
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"
- name: 'Azure PowerShell login without subscription'
uses: azure/login@v1
with:
creds: ${{secrets.AZURE_CREDENTIALS}}
enable-AzPSSession: true
allow-no-subscriptions: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"
az-login-test-oidc:
runs-on: ubuntu-latest
# continue-on-error: true
steps:
- name: 'Az CLI login with subscription'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }}
- run: |
az account show
# az webapp list
- name: 'Az CLI login without subscription'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
allow-no-subscriptions: true
- run: |
az account show
- name: 'Azure PowerShell login with subscription'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }}
enable-AzPSSession: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"
- name: 'Azure PowerShell login without subscription'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENTID }}
tenant-id: ${{ secrets.AZURE_TENANTID }}
enable-AzPSSession: true
allow-no-subscriptions: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"
slack-post-result:
runs-on: ubuntu-latest
# continue-on-error: true
if: ${{ always() }}
needs: [az-login-test-non-oidc, az-login-test-oidc]
steps:
- name: Create slack post
id: slack_report
run: |
TITLE="Login action OIDC flow tests update - "
DATEVAR=`date "+%d/%m/%YT%H:%M:%S"`
TITLE="${TITLE}${DATEVAR}"
REPORT="${TITLE}\r\nLink to run - https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\r\n"
RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
REPORT="${REPORT}\r\n"
if [ ${{needs.az-login-test-non-oidc.result}} == 'success' ]; then REPORT="${REPORT}\r\n|✅|<${RUN_URL}|az-login-test-non-oidc>"; else REPORT="${REPORT}\r\n|❌|<${RUN_URL}|az-login-test-non-oidc>"; fi
if [ ${{needs.az-login-test-oidc.result}} == 'success' ]; then REPORT="${REPORT}\r\n|✅|<${RUN_URL}|az-login-test-oidc>"; else REPORT="${REPORT}\r\n|❌|<${RUN_URL}|az-login-test-oidc>"; fi
echo "::set-output name=report::$REPORT"
- name: Post to slack
shell: bash
run: curl -X POST -H 'Content-type:application/json' --data '{"blocks":[{"type":"section","text":{"type":"mrkdwn","text":"${{steps.slack_report.outputs.report}}"}}]}' https://hooks.slack.com/services/${{SECRETS.SLACK_CHANNEL_SECRET}}

View File

@@ -1,72 +0,0 @@
name: pr-check
on:
pull_request_target:
branches:
- master
- 'releases/*'
jobs:
az-login-test:
environment: Automation test
runs-on: windows-latest
steps:
- name: Checkout from PR branch
uses: actions/checkout@v2
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
# Using 12.x version as an example
- name: Set Node.js 12.x for GitHub Action
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: installing node_modules
run: npm install
- name: Build GitHub Action
run: npm run build
- name: 'Az CLI login with subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az account show
# az webapp list
- name: 'Az CLI login without subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS_NO_SUB }}
allow-no-subscriptions: true
- run: |
az account show
- name: 'Azure PowerShell login with subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"
- name: 'Azure PowerShell login without subscription'
uses: ./
with:
creds: ${{secrets.AZURE_CREDENTIALS_NO_SUB}}
enable-AzPSSession: true
allow-no-subscriptions: true
- uses: azure/powershell@v1
with:
inlineScript: "Get-AzContext"
azPSVersion: "latest"

319
README.md
View File

@@ -2,34 +2,27 @@
## Automate your GitHub workflows using Azure Actions ## Automate your GitHub workflows using Azure Actions
[GitHub Actions](https://help.github.com/en/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow. [GitHub Actions](https://help.github.com/en/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow.
With [GitHub Actions for Azure](https://github.com/Azure/actions/) you can create workflows that you can set up in your repository to build, test, package, release and **deploy** to Azure. With [GitHub Actions for Azure](https://github.com/Azure/actions/) you can create workflows that you can set up in your repository to build, test, package, release and **deploy** to Azure.
NOTE: you must have write permissions to the repository in question. If you're using a sample repository from Microsoft, be sure to first fork the repository to your own GitHub account.
Get started today with a [free Azure account](https://azure.com/free/open-source).
# GitHub Action for Azure Login # GitHub Action for Azure Login
With the [Azure Login](https://github.com/Azure/login/blob/master/action.yml) Action, you can automate your workflow to do an Azure login using [Azure service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals) and run Az CLI and Azure PowerShell scripts. With the Azure login Action, you can automate your workflow to do an Azure login using [Azure service principal](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) and run Azure CLI and Azure PowerShell scripts. You can leverage this action for the public or soverign clouds including Azure Government and Azure Stack Hub (using the `environment` parameter).
- 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. 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.
- 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 [this](#configure-a-service-principal-with-a-secret) section below for details). To login into one of the Azure Government clouds, set the optional parameter environment with supported cloud names AzureUSGovernment or AzureChinaCloud. If this parameter is not specified, it takes the default value AzureCloud and connect to the Azure Public Cloud. Additionally the parameter creds takes the Azure service principal created in the particular cloud to connect (Refer to Configure deployment credentials 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 Service Principal with a Federated Identity Credential. This repository contains GitHub Action for [Azure Login](https://github.com/Azure/login/blob/master/action.yml).
- 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**,
1. Follow [this](#configure-a-service-principal-with-a-federated-credential-to-use-oidc-based-authentication) guidance to create a Federated Credential associated with your AD App (Service Principal). This is needed to establish OIDC trust between GitHub deployment workflows and the specific Azure resources scoped by the 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`, `tenant-id` and `subscription-id` of the Azure service principal associated with an OIDC Federated Identity Credential credeted in step (i)
Note:
- Ensure the CLI version is 2.30 or above to use OIDC support.
- OIDC support in Azure is supported only for public clouds. Support for other clouds like Government clouds, Azure Stacks would be added soon.
- By default, Azure access tokens issued during OIDC based login could have limited validity. This expiration time is configurable in Azure.
## Sample workflow that uses Azure login action to run az cli ## Sample workflow that uses Azure login action to run az cli
```yaml ```yaml
# File: .github/workflows/workflow.yml # File: .github/workflows/workflow.yml
on: [push] on: [push]
@@ -41,122 +34,63 @@ jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: azure/login@v1 - uses: azure/login@v1
with: with:
creds: ${{ secrets.AZURE_CREDENTIALS }} creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: | - run: |
az webapp list --query "[?state=='Running']" az webapp list --query "[?state=='Running']"
``` ```
## Sample workflow that uses Azure login action to run Azure PowerShell ## Sample workflow that uses Azure login action to run Azure PowerShell
```yaml ```yaml
# File: .github/workflows/workflow.yml # File: .github/workflows/workflow.yml
on: [push] on: [push]
name: AzurePowerShellLoginSample name: AzurePowerShellSample
jobs: jobs:
build: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Login via Az module - name: Login via Az module
uses: azure/login@v1 uses: azure/login@v1
with: with:
creds: ${{secrets.AZURE_CREDENTIALS}} creds: ${{secrets.AZURE_CREDENTIALS}}
enable-AzPSSession: true enable-AzPSSession: true
- run: | - name: Run Az CLI script
Get-AzVM -ResourceGroupName "ResourceGroup11" run: |
az webapp list --query "[?state=='Running']"
- name: Run Azure PowerShell script
uses: azure/powershell@v1
with:
azPSVersion: '3.1.0'
inlineScript: |
Get-AzVM -ResourceGroupName "ActionsDemo"
``` ```
## Sample workflow that uses Azure login action using OIDC to run az cli (Linux)
```yaml
# File: .github/workflows/OIDC_workflow.yml
name: Run Azure Login with OIDC
on: [push]
permissions:
id-token: write
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: 'Az CLI login'
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: 'Run az commands'
run: |
az account show
az group list
pwd
```
Users can also specify `audience` field for access-token in the input parameters of the action. If not specified, it is defaulted to `api://AzureADTokenExchange`. This action supports login az powershell as well for both windows and linux runners by setting an input parameter `enable-AzPSSession: true`. Below is the sample workflow for the same using the windows runner. Please note that powershell login is not supported in Macos runners.
## Sample workflow that uses Azure login action using OIDC to run az PowerShell (Windows)
```yaml
# File: .github/workflows/OIDC_workflow.yml
name: Run Azure Login with OIDC
on: [push]
permissions:
id-token: write
contents: read
jobs:
Windows-latest:
runs-on: windows-latest
steps:
- name: OIDC Login to Azure Public Cloud with AzPowershell (enableAzPSSession true)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
- name: 'Get RG with powershell action'
uses: azure/powershell@v1
with:
inlineScript: |
Get-AzResourceGroup
azPSVersion: "latest"
```
Refer [Azure PowerShell](https://github.com/azure/powershell) Github action to run your Azure PowerShell scripts.
## Sample to connect to Azure US Government cloud ## Sample to connect to Azure US Government cloud
```yaml ```
- name: Login to Azure US Gov Cloud with CLI - name: Login to Azure US Gov Cloud with CLI
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }}
environment: 'AzureUSGovernment'
enable-AzPSSession: false
- name: Login to Azure US Gov Cloud with Az Powershell
uses: azure/login@v1 uses: azure/login@v1
with: with:
creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }} creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }}
environment: 'AzureUSGovernment' environment: 'AzureUSGovernment'
enable-AzPSSession: false enable-AzPSSession: true
- name: Login to Azure US Gov Cloud with Az Powershell
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_US_GOV_CREDENTIALS }}
environment: 'AzureUSGovernment'
enable-AzPSSession: true
``` ```
Refer to the [Azure PowerShell](https://github.com/azure/powershell) Github action to run your Azure PowerShell scripts. Refer to the [Azure PowerShell](https://github.com/azure/powershell) Github action to run your Azure PowerShell scripts.
@@ -188,95 +122,99 @@ jobs:
Refer to the [Azure Stack Hub Login Action Tutorial](https://docs.microsoft.com/en-us/azure-stack/user/ci-cd-github-action-login-cli?view=azs-2008) for more detailed instructions. Refer to the [Azure Stack Hub Login Action Tutorial](https://docs.microsoft.com/en-us/azure-stack/user/ci-cd-github-action-login-cli?view=azs-2008) for more detailed instructions.
## Configure deployment credentials: ## Configure deployment credentials:
### Configure a service principal with a secret:
For using any credentials like Azure Service Principal, Publish Profile etc add them as [secrets](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables) in the GitHub repository and then use them in the workflow. The previous sample workflows depend on a [secrets](https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets) named `AZURE_CREDENTIALS` in your repository. The value of this secret is expected to be a JSON object that represents a service principal (an identifer for an application or process) that authenticates the workflow with Azure.
To function correctly, this service principal must be assigned the [Contributor]((https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#contributor)) role for the web app or the resource group that contains the web app.
Follow the steps to configure Azure Service Principal with a secret: The following steps describe how to create the service principal, assign the role, and create a secret in your repository with the resulting credentials.
* Define a new secret under your repository settings, Add secret menu
* Store the output of the below [az cli](https://docs.microsoft.com/en-us/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 \ 1. Open the Azure Cloud Shell at [https://shell.azure.com](https://shell.azure.com). You can alternately use the [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) if you've installed it locally. (For more information on Cloud Shell, see the [Cloud Shell Overview](https://docs.microsoft.com/azure/cloud-shell/overview).)
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
# Replace {subscription-id}, {resource-group} with the subscription, resource group details
# The command should output a JSON object similar to this: 1.1 **(Required ONLY when environment is Azure Stack Hub)** Run the following command to set the SQL Management endpoint to 'not supported'
```bash
az cloud update -n {environmentName} --endpoint-sql-management https://notsupported
{
"clientId": "<GUID>", ```
"clientSecret": "<STRING>",
"subscriptionId": "<GUID>", 2. Use the [az ad sp create-for-rbac](https://docs.microsoft.com/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac) command to create a service principal and assign a Contributor role:
"tenantId": "<GUID>",
"resourceManagerEndpointUrl": "<URL>" For web apps (also more secure)
(...)
} ```azurecli
az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor \
``` --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Web/sites/{app-name}
* 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.
* 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/en/actions/security-guides/encrypted-secrets) for Non-OIDC login, below snippet can help with the same. For usage with other Azure services (Storage Accounts, Active Directory, etc.)
```azurecli
az ad sp create-for-rbac --name "{sp-name}" --sdk-auth --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group}
```
Replace the following:
* `{sp-name}` with a suitable name for your service principal, such as the name of the app itself. The name must be unique within your organization.
* `{subscription-id}` with the subscription ID you want to use (found in Subscriptions in portal)
* `{resource-group}` the resource group containing the web app.
* [optional] `{app-name}` if you wish to have a tighter & more secure scope, use the first option and replace this with the name of the web app.
More info can be found [here](https://docs.microsoft.com/en-us/cli/azure/ad/sp?view=azure-cli-latest#az_ad_sp_create_for_rbac).
This command invokes Azure Active Directory (via the `ad` part of the command) to create a service principal (via `sp`) specifically for [Role-Based Access Control (RBAC)](https://docs.microsoft.com/azure/role-based-access-control/overview) (via `create-for-rbac`).
The `--role` argument specifies the permissions to grant to the service principal at the specified `--scope`. In this case, you grant the built-in [Contributor](https://docs.microsoft.com/azure/role-based-access-control/built-in-roles#contributor) role at the scope of the web app in the specified resource group in the specified subscription. If desired, you can omit the part of the scope starting with `/providers/...` to grant the service principal the Contributor role for the entire resource group. For security purposes, however, it's always preferable to grant permissions at the most restrictive scope possible.
3. When complete, the `az ad sp create-for-rbac` command displays JSON output in the following form (which is specified by the `--sdk-auth` argument):
```json
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)
}
```
4. In your repository, use **Add secret** to create a new secret named `AZURE_CREDENTIALS` (as shown in the example workflow), or using whatever name is in your workflow file.
NOTE: While adding secret `AZURE_CREDENTIALS` make sure to add like this
{"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)}
instead of
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
(...)
}
to prevent unnecessary masking of `{ } ` in your logs which are in dictionary form.
5. Paste the entire JSON object produced by the `az ad sp create-for-rbac` command as the secret value and save the secret.
NOTE: to manage service principals created with `az ad sp create-for-rbac`, visit the [Azure portal](https://portal.azure.com), navigate to your Azure Active Directory, then select **Manage** > **App registrations** on the left-hand menu. Your service principal should appear in the list. Select a principal to navigate to its properties. You can also manage role assignments using the [az role assignment](https://docs.microsoft.com/cli/azure/role/assignment?view=azure-cli-latest) command.
NOTE: Currently there is no support for passing in the Subscription ID, Tenant ID, Client ID, and Client Secret as individual parameters instead of bundled in a single JSON object (creds).
However, a simple workaround for users who need this option can be:
```yaml ```yaml
- uses: Azure/login@v1 - uses: Azure/login@v1.1
with: with:
creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}' creds: '{"clientId":"${{ secrets.CLIENT_ID }}","clientSecret":"${{ secrets.CLIENT_SECRET }}","subscriptionId":"${{ secrets.SUBSCRIPTION_ID }}","tenantId":"${{ secrets.TENANT_ID }}"}'
``` ```
In a similar way, any additional parameter can be added to creds such as resourceManagerEndpointUrl for Azure Stack, for example. In a similar way, any additional parameter can be addded to creds such as resourceManagerEndpointUrl for Azure Stack, for example.
### Manually creating the Credentials object
If you already created and assigned a Service Principal in Azure you can manually create the .json object above by finding the `clientId` and `clientSecret` on the Service Principal, and your `subscriptionId` and `tenantId` of the subscription and tenant respectively. The `resourceManagerEndpointUrl` will be `https://management.azure.com/` if you are using the public Azure cloud.
### Configure a service principal with a Federated Credential to use OIDC based authentication:
You can add federated credentials in the Azure portal or with the Microsoft Graph REST API.
#### Azure portal
1. [Register an application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) in Azure Portal
2. Within the registered application, Go to **Certificates & secrets**.
3. In the **Federated credentials** tab, select **Add credential**.
4. The **Add a credential** blade opens.
5. In the **Federated credential scenario** box select **GitHub actions deploying Azure resources**.
6. Specify the **Organization** and **Repository** for your GitHub Actions workflow which needs to access the Azure resources scoped by this App (Service Principal)
7. For **Entity type**, select **Environment**, **Branch**, **Pull request**, or **Tag** and specify the value, based on how you have configured the trigger for your GitHub workflow. For a more detailed overview, see [GitHub OIDC guidance]( https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#defining-[…]dc-claims).
8. Add a **Name** for the federated credential.
9. Click **Add** to configure the federated credential.
10. Make sure the above created application has the `contributor` access to the provided subscription. Visit [role-based-access-control](https://docs.microsoft.com/en-us/azure/role-based-access-control/role-assignments-portal?tabs=current#prerequisites) for more details.
For a more detailed overview, see more guidance around [Azure Federated Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation-create-trust-github).
#### Microsoft Graph
1. Launch [Azure Cloud Shell](https://portal.azure.com/#cloudshell/) and sign in to your tenant.
1. Create a federated identity credential
Run the following command to [create a new federated identity credential](https://docs.microsoft.com/en-us/graph/api/application-post-federatedidentitycredentials?view=graph-rest-beta&preserve-view=true) on your app (specified by the object ID of the app). Substitute the values `APPLICATION-OBJECT-ID`, `CREDENTIAL-NAME`, `SUBJECT`. The options for subject refer to your request filter. These are the conditions that OpenID Connect uses to determine when to issue an authentication token.
* specific environment
```azurecli
az rest --method POST --uri 'https://graph.microsoft.com/beta/applications/<APPLICATION-OBJECT-ID>/federatedIdentityCredentials' --body '{"name":"<CREDENTIAL-NAME>","issuer":"https://token.actions.githubusercontent.com","subject":"repo:octo-org/octo-repo:environment:Production","description":"Testing","audiences":["api://AzureADTokenExchange"]}'
```
* pull_request events
```azurecli
az rest --method POST --uri 'https://graph.microsoft.com/beta/applications/<APPLICATION-OBJECT-ID>/federatedIdentityCredentials' --body '{"name":"<CREDENTIAL-NAME>","issuer":"https://token.actions.githubusercontent.com","subject":"repo:octo-org/octo-repo:pull_request","description":"Testing","audiences":["api://AzureADTokenExchange"]}'
```
* specific branch
```azurecli
az rest --method POST --uri 'https://graph.microsoft.com/beta/applications/<APPLICATION-OBJECT-ID>/federatedIdentityCredentials' --body '{"name":"<CREDENTIAL-NAME>","issuer":"https://token.actions.githubusercontent.com","subject":"repo:octo-org/octo-repo:ref:refs/heads/{Branch}","description":"Testing","audiences":["api://AzureADTokenExchange"]}'
```
* specific tag
```azurecli
az rest --method POST --uri 'https://graph.microsoft.com/beta/applications/<APPLICATION-OBJECT-ID>/federatedIdentityCredentials' --body '{"name":"<CREDENTIAL-NAME>","issuer":"https://token.actions.githubusercontent.com","subject":"repo:octo-org/octo-repo:ref:refs/heads/{Tag}","description":"Testing","audiences":["api://AzureADTokenExchange"]}'
```
## Support for using `allow-no-subscriptions` flag with az login ## 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 ```yaml
# File: .github/workflows/workflow.yml # File: .github/workflows/workflow.yml
@@ -303,23 +241,18 @@ This action doesn't implement ```az logout``` by default at the end of execution
- name: Azure CLI script - name: Azure CLI script
uses: azure/CLI@v1 uses: azure/CLI@v1
with: with:
azcliversion: 2.0.72
inlineScript: | inlineScript: |
az logout az logout
az cache purge az cache purge
az account clear az account clear
``` ```
## 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 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 # Contributing
This project welcomes contributions and suggestions. Most contributions require you to agree to a This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us
the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide For detailed developer guidelines, visit [developer guidelines for azure actions](https://github.com/Azure/actions/blob/main/docs/developer-guildelines.md).
a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions
provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.

View File

@@ -5,7 +5,7 @@ jest.mock('../../src/PowerShell/Utilities/PowerShellToolRunner');
let spnlogin: ServicePrincipalLogin; let spnlogin: ServicePrincipalLogin;
beforeAll(() => { beforeAll(() => {
spnlogin = new ServicePrincipalLogin("servicePrincipalID", "servicePrinicipalkey", null, "tenantId", "subscriptionId", false, null, null); spnlogin = new ServicePrincipalLogin("servicePrincipalID", "servicePrinicipalkey", "tenantId", "subscriptionId", false, null, null);
}); });
afterEach(() => { afterEach(() => {

View File

@@ -4,16 +4,7 @@ description: 'AuthenticatetoAzureandrunyourAzCLIorAz
inputs: inputs:
creds: creds:
description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS' description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS'
required: false required: true
client-id:
description: 'ClientId of the Azure Service principal created.'
required: false
tenant-id:
description: 'TenantId of the Azure Service principal created.'
required: false
subscription-id:
description: 'Azure subscriptionId'
required: false
enable-AzPSSession: enable-AzPSSession:
description: 'SetthisvaluetotruetoenableAzurePowerShellLogininadditiontoAzCLIlogin' description: 'SetthisvaluetotruetoenableAzurePowerShellLogininadditiontoAzCLIlogin'
required: false required: false
@@ -21,18 +12,14 @@ inputs:
environment: environment:
description: 'Name of the environment. Supported values are azurecloud, azurestack, azureusgovernment, azurechinacloud, azuregermancloud. Default being azurecloud' description: 'Name of the environment. Supported values are azurecloud, azurestack, azureusgovernment, azurechinacloud, azuregermancloud. Default being azurecloud'
required: false required: false
default: azurecloud default: AzureCloud
allow-no-subscriptions: allow-no-subscriptions:
description: 'Setthisvaluetotrueto enable support for accessing tenants without subscriptions' description: 'Setthisvaluetotrueto enable support for accessing tenants without subscriptions'
required: false required: false
default: false default: false
audience:
description: 'Provide audience field for access-token. Default value is api://AzureADTokenExchange'
required: false
default: 'api://AzureADTokenExchange'
branding: branding:
icon: 'login.svg' icon: 'login.svg'
color: 'blue' color: 'blue'
runs: runs:
using: 'node16' using: 'node12'
main: 'lib/main.js' main: 'lib/main.js'

View File

@@ -5,7 +5,7 @@ class Constants {
exports.default = Constants; exports.default = Constants;
Constants.prefix = "az_"; Constants.prefix = "az_";
Constants.moduleName = "Az.Accounts"; Constants.moduleName = "Az.Accounts";
Constants.versionPattern = /[0-9]+\.[0-9]+\.[0-9]+/; Constants.versionPattern = /[0-9]\.[0-9]\.[0-9]/;
Constants.AzureCloud = "AzureCloud"; Constants.AzureCloud = "AzureCloud";
Constants.Subscription = "Subscription"; Constants.Subscription = "Subscription";
Constants.ServicePrincipal = "ServicePrincipal"; Constants.ServicePrincipal = "ServicePrincipal";

View File

@@ -1,23 +1,4 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) { return new (P || (P = Promise))(function (resolve, reject) {
@@ -27,21 +8,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) { var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ServicePrincipalLogin = void 0;
const core = __importStar(require("@actions/core")); const core = __importStar(require("@actions/core"));
const Utils_1 = __importDefault(require("./Utilities/Utils")); const Utils_1 = __importDefault(require("./Utilities/Utils"));
const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellToolRunner")); const PowerShellToolRunner_1 = __importDefault(require("./Utilities/PowerShellToolRunner"));
const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder")); const ScriptBuilder_1 = __importDefault(require("./Utilities/ScriptBuilder"));
const Constants_1 = __importDefault(require("./Constants")); const Constants_1 = __importDefault(require("./Constants"));
class ServicePrincipalLogin { class ServicePrincipalLogin {
constructor(servicePrincipalId, servicePrincipalKey, federatedToken, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl) { constructor(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl) {
this.servicePrincipalId = servicePrincipalId; this.servicePrincipalId = servicePrincipalId;
this.servicePrincipalKey = servicePrincipalKey; this.servicePrincipalKey = servicePrincipalKey;
this.federatedToken = federatedToken;
this.tenantId = tenantId; this.tenantId = tenantId;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.environment = environment; this.environment = environment;
@@ -59,25 +45,16 @@ class ServicePrincipalLogin {
login() { login() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
let output = ""; let output = "";
let commandStdErr = false;
const options = { const options = {
listeners: { listeners: {
stdout: (data) => { stdout: (data) => {
output += data.toString(); output += data.toString();
},
stderr: (data) => {
let error = data.toString();
if (error && error.trim().length !== 0) {
commandStdErr = true;
core.error(error);
}
} }
} }
}; };
const args = { const args = {
servicePrincipalId: this.servicePrincipalId, servicePrincipalId: this.servicePrincipalId,
servicePrincipalKey: this.servicePrincipalKey, servicePrincipalKey: this.servicePrincipalKey,
federatedToken: this.federatedToken,
subscriptionId: this.subscriptionId, subscriptionId: this.subscriptionId,
environment: this.environment, environment: this.environment,
scopeLevel: ServicePrincipalLogin.scopeLevel, scopeLevel: ServicePrincipalLogin.scopeLevel,

View File

@@ -1,23 +1,4 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) { return new (P || (P = Promise))(function (resolve, reject) {
@@ -27,6 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const io = __importStar(require("@actions/io")); const io = __importStar(require("@actions/io"));
const exec = __importStar(require("@actions/exec")); const exec = __importStar(require("@actions/exec"));
@@ -40,7 +28,6 @@ class PowerShellToolRunner {
} }
static executePowerShellScriptBlock(scriptBlock, options = {}) { static executePowerShellScriptBlock(scriptBlock, options = {}) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
//Options for error handling
yield exec.exec(`"${PowerShellToolRunner.psPath}" -Command`, [scriptBlock], options); yield exec.exec(`"${PowerShellToolRunner.psPath}" -Command`, [scriptBlock], options);
}); });
} }

View File

@@ -1,21 +1,9 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) { var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod; if (mod && mod.__esModule) return mod;
var result = {}; var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
__setModuleDefault(result, mod); result["default"] = mod;
return result; return result;
}; };
var __importDefault = (this && this.__importDefault) || function (mod) { var __importDefault = (this && this.__importDefault) || function (mod) {
@@ -35,17 +23,9 @@ class ScriptBuilder {
if (args.environment.toLowerCase() == "azurestack") { if (args.environment.toLowerCase() == "azurestack") {
command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`;
} }
// Separate command script for OIDC and non-OIDC command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
if (!!args.federatedToken) { (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
command += `Connect-AzAccount -ServicePrincipal -ApplicationId '${args.servicePrincipalId}' -Tenant '${tenantId}' -FederatedToken '${args.federatedToken}' \ -Environment '${args.environment}' | out-null;`;
-Environment '${args.environment}' | out-null;`;
}
else {
command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
(New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
-Environment '${args.environment}' | out-null;`;
}
// command to set the subscription
if (args.scopeLevel === Constants_1.default.Subscription && !args.allowNoSubscriptionsLogin) { if (args.scopeLevel === Constants_1.default.Subscription && !args.allowNoSubscriptionsLogin) {
command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`; command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`;
} }

View File

@@ -1,23 +1,4 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) { return new (P || (P = Promise))(function (resolve, reject) {
@@ -27,6 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) { var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };

View File

@@ -1,23 +1,4 @@
"use strict"; "use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) { return new (P || (P = Promise))(function (resolve, reject) {
@@ -27,6 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next()); step((generator = generator.apply(thisArg, _arguments || [])).next());
}); });
}; };
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(require("@actions/core")); const core = __importStar(require("@actions/core"));
const exec = __importStar(require("@actions/exec")); const exec = __importStar(require("@actions/exec"));
@@ -39,25 +27,6 @@ var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREP
function main() { function main() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
//Options for error handling
const loginOptions = {
silent: true,
listeners: {
stderr: (data) => {
let error = data.toString();
let startsWithWarning = error.toLowerCase().startsWith('warning');
let startsWithError = error.toLowerCase().startsWith('error');
// printing ERROR
if (error && error.trim().length !== 0 && !startsWithWarning) {
if (startsWithError) {
//removing the keyword 'ERROR' to avoid duplicates while throwing error
error = error.slice(5);
}
core.setFailed(error);
}
}
}
};
// Set user agent variable // Set user agent variable
var isAzCLISuccess = false; var isAzCLISuccess = false;
let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`; let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`;
@@ -67,7 +36,6 @@ function main() {
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString);
core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv);
azPath = yield io.which("az", true); azPath = yield io.which("az", true);
core.debug(`az cli version used: ${azPath}`);
let azureSupportedCloudName = new Set([ let azureSupportedCloudName = new Set([
"azureusgovernment", "azureusgovernment",
"azurechinacloud", "azurechinacloud",
@@ -85,68 +53,25 @@ function main() {
}; };
yield executeAzCliCommand("--version", true, execOptions); yield executeAzCliCommand("--version", true, execOptions);
core.debug(`az cli version used:\n${output}`); core.debug(`az cli version used:\n${output}`);
let creds = core.getInput('creds', { required: false }); let creds = core.getInput('creds', { required: true });
let secrets = creds ? new actions_secret_parser_1.SecretParser(creds, actions_secret_parser_1.FormatType.JSON) : null; let secrets = new actions_secret_parser_1.SecretParser(creds, actions_secret_parser_1.FormatType.JSON);
let servicePrincipalId = secrets.getSecret("$.clientId", false);
let servicePrincipalKey = secrets.getSecret("$.clientSecret", true);
let tenantId = secrets.getSecret("$.tenantId", false);
let subscriptionId = secrets.getSecret("$.subscriptionId", false);
let resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false);
let environment = core.getInput("environment").toLowerCase(); let environment = core.getInput("environment").toLowerCase();
const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true";
const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true";
//Check for the credentials in individual parameters in the workflow. if (!servicePrincipalId || !servicePrincipalKey || !tenantId) {
var servicePrincipalId = core.getInput('client-id', { required: false }); throw new Error("Not all values are present in the creds object. Ensure clientId, clientSecret and tenantId are supplied.");
var servicePrincipalKey = null;
var tenantId = core.getInput('tenant-id', { required: false });
var subscriptionId = core.getInput('subscription-id', { required: false });
var resourceManagerEndpointUrl = "https://management.azure.com/";
var enableOIDC = true;
var federatedToken = null;
// If any of the individual credentials (clent_id, tenat_id, subscription_id) is present.
if (servicePrincipalId || tenantId || subscriptionId) {
//If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs.
if (!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin)))
throw new Error("Few credentials are missing. ClientId, tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set.");
}
else {
if (creds) {
core.debug('using creds JSON...');
enableOIDC = false;
servicePrincipalId = secrets.getSecret("$.clientId", true);
servicePrincipalKey = secrets.getSecret("$.clientSecret", true);
tenantId = secrets.getSecret("$.tenantId", true);
subscriptionId = secrets.getSecret("$.subscriptionId", true);
resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false);
}
else {
throw new Error("Credentials are not passed for Login action.");
}
}
//generic checks
//servicePrincipalKey is only required in non-oidc scenario.
if (!servicePrincipalId || !tenantId || !(servicePrincipalKey || enableOIDC)) {
throw new Error("Not all values are present in the credentials. Ensure clientId, clientSecret and tenantId are supplied.");
} }
if (!subscriptionId && !allowNoSubscriptionsLogin) { if (!subscriptionId && !allowNoSubscriptionsLogin) {
throw new Error("Not all values are present in the credentials. Ensure subscriptionId is supplied."); throw new Error("Not all values are present in the creds object. Ensure subscriptionId is supplied.");
} }
if (!azureSupportedCloudName.has(environment)) { if (!azureSupportedCloudName.has(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");
} }
// OIDC specific checks
if (enableOIDC) {
console.log('Using OIDC authentication...');
try {
//generating ID-token
let audience = core.getInput('audience', { required: false });
federatedToken = yield core.getIDToken(audience);
if (!!federatedToken) {
if (environment != "azurecloud")
throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`);
let [issuer, subjectClaim] = yield jwtParser(federatedToken);
console.log("Federated token details: \n issuer - " + issuer + " \n subject claim - " + subjectClaim);
}
}
catch (error) {
core.error(`${error.message.split(':')[1]}. Please make sure to give write permissions to id-token in the workflow.`);
}
}
// Attempting Az cli login // Attempting Az cli login
if (environment == "azurestack") { if (environment == "azurestack") {
if (!resourceManagerEndpointUrl) { if (!resourceManagerEndpointUrl) {
@@ -179,34 +104,35 @@ function main() {
yield executeAzCliCommand(`cloud set -n "${environment}"`, false); yield executeAzCliCommand(`cloud set -n "${environment}"`, false);
console.log(`Done setting cloud: "${environment}"`); console.log(`Done setting cloud: "${environment}"`);
// Attempting Az cli login // Attempting Az cli login
var commonArgs = ["--service-principal",
"-u", servicePrincipalId,
"--tenant", tenantId
];
if (allowNoSubscriptionsLogin) { if (allowNoSubscriptionsLogin) {
commonArgs = commonArgs.concat("--allow-no-subscriptions"); let args = [
} "--allow-no-subscriptions",
if (enableOIDC) { "--service-principal",
commonArgs = commonArgs.concat("--federated-token", federatedToken); "-u", servicePrincipalId,
"-p", servicePrincipalKey,
"--tenant", tenantId
];
yield executeAzCliCommand(`login`, true, {}, args);
} }
else { 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."); let args = [
commonArgs = commonArgs.concat("-p", servicePrincipalKey); "--service-principal",
} "-u", servicePrincipalId,
yield executeAzCliCommand(`login`, true, loginOptions, commonArgs); "-p", servicePrincipalKey,
if (!allowNoSubscriptionsLogin) { "--tenant", tenantId
var args = [ ];
yield executeAzCliCommand(`login`, true, {}, args);
args = [
"--subscription", "--subscription",
subscriptionId subscriptionId
]; ];
yield executeAzCliCommand(`account set`, true, loginOptions, args); yield executeAzCliCommand(`account set`, true, {}, args);
} }
isAzCLISuccess = true; isAzCLISuccess = true;
if (enableAzPSSession) { if (enableAzPSSession) {
// Attempting Az PS login // Attempting Az PS login
console.log(`Running Azure PS Login`); console.log(`Running Azure PS Login`);
var spnlogin; const spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl);
spnlogin = new ServicePrincipalLogin_1.ServicePrincipalLogin(servicePrincipalId, servicePrincipalKey, federatedToken, tenantId, subscriptionId, allowNoSubscriptionsLogin, environment, resourceManagerEndpointUrl);
yield spnlogin.initialize(); yield spnlogin.initialize();
yield spnlogin.login(); yield spnlogin.login();
} }
@@ -214,11 +140,12 @@ function main() {
} }
catch (error) { catch (error) {
if (!isAzCLISuccess) { if (!isAzCLISuccess) {
core.setFailed("Az CLI Login failed. 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.error("Az CLI Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows");
} }
else { else {
core.setFailed(`Azure PowerShell Login failed. 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.error(`Azure PowerShell Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"`);
} }
core.setFailed(error);
} }
finally { finally {
// Reset AZURE_HTTP_USER_AGENT // Reset AZURE_HTTP_USER_AGENT
@@ -230,15 +157,12 @@ function main() {
function executeAzCliCommand(command, silent, execOptions = {}, args = []) { function executeAzCliCommand(command, silent, execOptions = {}, args = []) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
execOptions.silent = !!silent; execOptions.silent = !!silent;
yield exec.exec(`"${azPath}" ${command}`, args, execOptions); try {
}); yield exec.exec(`"${azPath}" ${command}`, args, execOptions);
} }
function jwtParser(federatedToken) { catch (error) {
return __awaiter(this, void 0, void 0, function* () { throw new Error(error);
let tokenPayload = federatedToken.split('.')[1]; }
let bufferObj = Buffer.from(tokenPayload, "base64");
let decodedPayload = JSON.parse(bufferObj.toString("utf8"));
return [decodedPayload['iss'], decodedPayload['sub']];
}); });
} }
main(); main();

9633
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "login", "name": "login",
"version": "1.0.0", "version": "1.1.0",
"description": "Login Azure wraps the az login, allowing for Azure actions to log into Azure", "description": "Login Azure wraps the az login, allowing for Azure actions to log into Azure",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
@@ -18,10 +18,9 @@
"typescript": "^3.6.3" "typescript": "^3.6.3"
}, },
"dependencies": { "dependencies": {
"@actions/core": "1.6.0", "@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",
"actions-secret-parser": "^1.0.2", "actions-secret-parser": "^1.0.2"
"package-lock": "^1.0.3"
} }
} }

View File

@@ -1,7 +1,7 @@
export default class Constants { export default class Constants {
static readonly prefix: string = "az_"; static readonly prefix: string = "az_";
static readonly moduleName: string = "Az.Accounts"; static readonly moduleName: string = "Az.Accounts";
static readonly versionPattern = /[0-9]+\.[0-9]+\.[0-9]+/; static readonly versionPattern = /[0-9]\.[0-9]\.[0-9]/;
static readonly AzureCloud: string = "AzureCloud"; static readonly AzureCloud: string = "AzureCloud";
static readonly Subscription: string = "Subscription"; static readonly Subscription: string = "Subscription";
@@ -10,4 +10,4 @@ export default class Constants {
static readonly Success: string = "Success"; static readonly Success: string = "Success";
static readonly Error: string = "Error"; static readonly Error: string = "Error";
static readonly AzVersion: string = "AzVersion"; static readonly AzVersion: string = "AzVersion";
} }

View File

@@ -15,20 +15,17 @@ export class ServicePrincipalLogin implements IAzurePowerShellSession {
subscriptionId: string; subscriptionId: string;
resourceManagerEndpointUrl: string; resourceManagerEndpointUrl: string;
allowNoSubscriptionsLogin: boolean; allowNoSubscriptionsLogin: boolean;
federatedToken: string;
constructor(servicePrincipalId: string, constructor(servicePrincipalId: string,
servicePrincipalKey: string, servicePrincipalKey: string,
federatedToken: string, tenantId: string,
tenantId: string,
subscriptionId: string, subscriptionId: string,
allowNoSubscriptionsLogin: boolean, allowNoSubscriptionsLogin: boolean,
environment: string, environment: string,
resourceManagerEndpointUrl: string) { resourceManagerEndpointUrl: string) {
this.servicePrincipalId = servicePrincipalId; this.servicePrincipalId = servicePrincipalId;
this.servicePrincipalKey = servicePrincipalKey; this.servicePrincipalKey = servicePrincipalKey;
this.federatedToken = federatedToken;
this.tenantId = tenantId; this.tenantId = tenantId;
this.subscriptionId = subscriptionId; this.subscriptionId = subscriptionId;
this.environment = environment; this.environment = environment;
@@ -45,26 +42,16 @@ export class ServicePrincipalLogin implements IAzurePowerShellSession {
async login() { async login() {
let output: string = ""; let output: string = "";
let commandStdErr = false;
const options: any = { const options: any = {
listeners: { listeners: {
stdout: (data: Buffer) => { stdout: (data: Buffer) => {
output += data.toString(); output += data.toString();
},
stderr: (data: Buffer) => {
let error = data.toString();
if (error && error.trim().length !== 0)
{
commandStdErr = true;
core.error(error);
}
} }
} }
}; };
const args: any = { const args: any = {
servicePrincipalId: this.servicePrincipalId, servicePrincipalId: this.servicePrincipalId,
servicePrincipalKey: this.servicePrincipalKey, servicePrincipalKey: this.servicePrincipalKey,
federatedToken: this.federatedToken,
subscriptionId: this.subscriptionId, subscriptionId: this.subscriptionId,
environment: this.environment, environment: this.environment,
scopeLevel: ServicePrincipalLogin.scopeLevel, scopeLevel: ServicePrincipalLogin.scopeLevel,

View File

@@ -3,6 +3,7 @@ import * as exec from '@actions/exec';
export default class PowerShellToolRunner { export default class PowerShellToolRunner {
static psPath: string; static psPath: string;
static async init() { static async init() {
if(!PowerShellToolRunner.psPath) { if(!PowerShellToolRunner.psPath) {
PowerShellToolRunner.psPath = await io.which("pwsh", true); PowerShellToolRunner.psPath = await io.which("pwsh", true);
@@ -10,7 +11,6 @@ export default class PowerShellToolRunner {
} }
static async executePowerShellScriptBlock(scriptBlock: string, options: any = {}) { static async executePowerShellScriptBlock(scriptBlock: string, options: any = {}) {
//Options for error handling
await exec.exec(`"${PowerShellToolRunner.psPath}" -Command`, [scriptBlock], options) await exec.exec(`"${PowerShellToolRunner.psPath}" -Command`, [scriptBlock], options)
} }
} }

View File

@@ -8,28 +8,17 @@ export default class ScriptBuilder {
getAzPSLoginScript(scheme: string, tenantId: string, args: any): string { getAzPSLoginScript(scheme: string, tenantId: string, args: any): string {
let command = `Clear-AzContext -Scope Process; let command = `Clear-AzContext -Scope Process;
Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`; Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue;`;
if (scheme === Constants.ServicePrincipal) { if (scheme === Constants.ServicePrincipal) {
if (args.environment.toLowerCase() == "azurestack") { if (args.environment.toLowerCase() == "azurestack") {
command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`; command += `Add-AzEnvironment -Name ${args.environment} -ARMEndpoint ${args.resourceManagerEndpointUrl} | out-null;`;
} }
// Separate command script for OIDC and non-OIDC command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
if (!!args.federatedToken) { (New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
command += `Connect-AzAccount -ServicePrincipal -ApplicationId '${args.servicePrincipalId}' -Tenant '${tenantId}' -FederatedToken '${args.federatedToken}' \ -Environment '${args.environment}' | out-null;`;
-Environment '${args.environment}' | out-null;`;
}
else {
command += `Connect-AzAccount -ServicePrincipal -Tenant '${tenantId}' -Credential \
(New-Object System.Management.Automation.PSCredential('${args.servicePrincipalId}',(ConvertTo-SecureString '${args.servicePrincipalKey.replace("'", "''")}' -AsPlainText -Force))) \
-Environment '${args.environment}' | out-null;`;
}
// command to set the subscription
if (args.scopeLevel === Constants.Subscription && !args.allowNoSubscriptionsLogin) { if (args.scopeLevel === Constants.Subscription && !args.allowNoSubscriptionsLogin) {
command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`; command += `Set-AzContext -SubscriptionId '${args.subscriptionId}' -TenantId '${tenantId}' | out-null;`;
} }
} }
this.script += `try { this.script += `try {
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$WarningPreference = "SilentlyContinue" $WarningPreference = "SilentlyContinue"
@@ -41,7 +30,6 @@ export default class ScriptBuilder {
$output['${Constants.Error}'] = $_.exception.Message $output['${Constants.Error}'] = $_.exception.Message
} }
return ConvertTo-Json $output`; return ConvertTo-Json $output`;
core.debug(`Azure PowerShell Login Script: ${this.script}`); core.debug(`Azure PowerShell Login Script: ${this.script}`);
return this.script; return this.script;
} }
@@ -63,5 +51,4 @@ export default class ScriptBuilder {
core.debug(`GetLatestModuleScript: ${this.script}`); core.debug(`GetLatestModuleScript: ${this.script}`);
return this.script; return this.script;
} }
}
}

View File

@@ -1,239 +1,176 @@
import * as core from '@actions/core'; import * as core from '@actions/core';
import * as exec from '@actions/exec'; import * as exec from '@actions/exec';
import { ExecOptions } from '@actions/exec/lib/interfaces'; import * as io from '@actions/io';
import * as io from '@actions/io'; import { FormatType, SecretParser } from 'actions-secret-parser';
import { FormatType, SecretParser } from 'actions-secret-parser'; import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin';
import { ServicePrincipalLogin } from './PowerShell/ServicePrincipalLogin';
var azPath: string;
var azPath: string; var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : "";
var prefix = !!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT}` : ""; var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT}` : "";
var azPSHostEnv = !!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT}` : "";
async function main() {
async function main() { try {
try { // Set user agent variable
//Options for error handling var isAzCLISuccess = false;
const loginOptions: ExecOptions = { let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`;
silent: true, let actionName = 'AzureLogin';
listeners: { let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
stderr: (data: Buffer) => { let azurePSHostEnv = (!!azPSHostEnv ? `${azPSHostEnv}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
let error = data.toString(); core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString);
let startsWithWarning = error.toLowerCase().startsWith('warning'); core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv);
let startsWithError = error.toLowerCase().startsWith('error');
// printing ERROR azPath = await io.which("az", true);
if (error && error.trim().length !== 0 && !startsWithWarning) {
if(startsWithError) { let azureSupportedCloudName = new Set([
//removing the keyword 'ERROR' to avoid duplicates while throwing error "azureusgovernment",
error = error.slice(5); "azurechinacloud",
} "azuregermancloud",
core.setFailed(error); "azurecloud",
} "azurestack"]);
}
} let output: string = "";
} const execOptions: any = {
// Set user agent variable listeners: {
var isAzCLISuccess = false; stdout: (data: Buffer) => {
let usrAgentRepo = `${process.env.GITHUB_REPOSITORY}`; output += data.toString();
let actionName = 'AzureLogin'; }
let userAgentString = (!!prefix ? `${prefix}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`; }
let azurePSHostEnv = (!!azPSHostEnv ? `${azPSHostEnv}+` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`; };
core.exportVariable('AZURE_HTTP_USER_AGENT', userAgentString); await executeAzCliCommand("--version", true, execOptions);
core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azurePSHostEnv); core.debug(`az cli version used:\n${output}`);
azPath = await io.which("az", true); let creds = core.getInput('creds', { required: true });
core.debug(`az cli version used: ${azPath}`); let secrets = new SecretParser(creds, FormatType.JSON);
let azureSupportedCloudName = new Set([ let servicePrincipalId = secrets.getSecret("$.clientId", false);
"azureusgovernment", let servicePrincipalKey = secrets.getSecret("$.clientSecret", true);
"azurechinacloud", let tenantId = secrets.getSecret("$.tenantId", false);
"azuregermancloud", let subscriptionId = secrets.getSecret("$.subscriptionId", false);
"azurecloud", let resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false);
"azurestack"]); let environment = core.getInput("environment").toLowerCase();
const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true";
let output: string = ""; const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true";
const execOptions: any = {
listeners: { if (!servicePrincipalId || !servicePrincipalKey || !tenantId) {
stdout: (data: Buffer) => { throw new Error("Not all values are present in the creds object. Ensure clientId, clientSecret and tenantId are supplied.");
output += data.toString(); }
}
} if (!subscriptionId && !allowNoSubscriptionsLogin) {
}; throw new Error("Not all values are present in the creds object. Ensure subscriptionId is supplied.");
await executeAzCliCommand("--version", true, execOptions); }
core.debug(`az cli version used:\n${output}`);
if (!azureSupportedCloudName.has(environment)){
let creds = core.getInput('creds', { required: false }); throw new Error("Unsupported value for environment is passed.The list of supported values for environment are azureusgovernment', azurechinacloud, azuregermancloud, azurecloud or azurestack");
let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null; }
let environment = core.getInput("environment").toLowerCase();
const enableAzPSSession = core.getInput('enable-AzPSSession').toLowerCase() === "true"; // Attempting Az cli login
const allowNoSubscriptionsLogin = core.getInput('allow-no-subscriptions').toLowerCase() === "true"; if (environment == "azurestack") {
if (!resourceManagerEndpointUrl) {
//Check for the credentials in individual parameters in the workflow. throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined.");
var servicePrincipalId = core.getInput('client-id', { required: false }); }
var servicePrincipalKey = null;
var tenantId = core.getInput('tenant-id', { required: false }); console.log(`Unregistering cloud: "${environment}" first if it exists`);
var subscriptionId = core.getInput('subscription-id', { required: false }); try {
var resourceManagerEndpointUrl = "https://management.azure.com/"; await executeAzCliCommand(`cloud set -n AzureCloud`, true);
var enableOIDC = true; await executeAzCliCommand(`cloud unregister -n "${environment}"`, false);
var federatedToken = null; }
catch (error) {
// If any of the individual credentials (clent_id, tenat_id, subscription_id) is present. console.log(`Ignore cloud not registered error: "${error}"`);
if (servicePrincipalId || tenantId || subscriptionId) { }
//If few of the individual credentials (clent_id, tenat_id, subscription_id) are missing in action inputs. console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`);
if (!(servicePrincipalId && tenantId && (subscriptionId || allowNoSubscriptionsLogin))) try {
throw new Error("Few credentials are missing. ClientId, tenantId are mandatory. SubscriptionId is also mandatory if allow-no-subscriptions is not set."); let baseUri = resourceManagerEndpointUrl;
} if (baseUri.endsWith('/')) {
else { baseUri = baseUri.substring(0, baseUri.length-1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below
if (creds) { }
core.debug('using creds JSON...'); let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with .
enableOIDC = false; let suffixStorage = baseUri.substring(baseUri.indexOf('.')+1); // storage suffix starts without .
servicePrincipalId = secrets.getSecret("$.clientId", true); let profileVersion = "2019-03-01-hybrid";
servicePrincipalKey = secrets.getSecret("$.clientSecret", true); await executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false);
tenantId = secrets.getSecret("$.tenantId", true); }
subscriptionId = secrets.getSecret("$.subscriptionId", true); catch (error) {
resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false); core.error(`Error while trying to register cloud "${environment}": "${error}"`);
} }
else {
throw new Error("Credentials are not passed for Login action."); console.log(`Done registering cloud: "${environment}"`)
} }
}
//generic checks await executeAzCliCommand(`cloud set -n "${environment}"`, false);
//servicePrincipalKey is only required in non-oidc scenario. console.log(`Done setting cloud: "${environment}"`);
if (!servicePrincipalId || !tenantId || !(servicePrincipalKey || enableOIDC)) {
throw new Error("Not all values are present in the credentials. Ensure clientId, clientSecret and tenantId are supplied."); // Attempting Az cli login
} if (allowNoSubscriptionsLogin) {
if (!subscriptionId && !allowNoSubscriptionsLogin) { let args = [
throw new Error("Not all values are present in the credentials. Ensure subscriptionId is supplied."); "--allow-no-subscriptions",
} "--service-principal",
if (!azureSupportedCloudName.has(environment)) { "-u", servicePrincipalId,
throw new Error("Unsupported value for environment is passed.The list of supported values for environment are azureusgovernment', azurechinacloud, azuregermancloud, azurecloud or azurestack"); "-p", servicePrincipalKey,
} "--tenant", tenantId
];
// OIDC specific checks await executeAzCliCommand(`login`, true, {}, args);
if (enableOIDC) { }
console.log('Using OIDC authentication...') else {
try { let args = [
//generating ID-token "--service-principal",
let audience = core.getInput('audience', { required: false }); "-u", servicePrincipalId,
federatedToken = await core.getIDToken(audience); "-p", servicePrincipalKey,
if (!!federatedToken) { "--tenant", tenantId
if (environment != "azurecloud") ];
throw new Error(`Your current environment - "${environment}" is not supported for OIDC login.`); await executeAzCliCommand(`login`, true, {}, args);
let [issuer, subjectClaim] = await jwtParser(federatedToken); args = [
console.log("Federated token details: \n issuer - " + issuer + " \n subject claim - " + subjectClaim); "--subscription",
} subscriptionId
} ];
catch (error) { await executeAzCliCommand(`account set`, true, {}, args);
core.error(`${error.message.split(':')[1]}. Please make sure to give write permissions to id-token in the workflow.`); }
}
} isAzCLISuccess = true;
if (enableAzPSSession) {
// Attempting Az cli login // Attempting Az PS login
if (environment == "azurestack") { console.log(`Running Azure PS Login`);
if (!resourceManagerEndpointUrl) { const spnlogin: ServicePrincipalLogin = new ServicePrincipalLogin(
throw new Error("resourceManagerEndpointUrl is a required parameter when environment is defined."); servicePrincipalId,
} servicePrincipalKey,
tenantId,
console.log(`Unregistering cloud: "${environment}" first if it exists`); subscriptionId,
try { allowNoSubscriptionsLogin,
await executeAzCliCommand(`cloud set -n AzureCloud`, true); environment,
await executeAzCliCommand(`cloud unregister -n "${environment}"`, false); resourceManagerEndpointUrl);
} await spnlogin.initialize();
catch (error) { await spnlogin.login();
console.log(`Ignore cloud not registered error: "${error}"`); }
}
console.log("Login successful.");
console.log(`Registering cloud: "${environment}" with ARM endpoint: "${resourceManagerEndpointUrl}"`); }
try { catch (error) {
let baseUri = resourceManagerEndpointUrl; if (!isAzCLISuccess) {
if (baseUri.endsWith('/')) { core.error("Az CLI Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows");
baseUri = baseUri.substring(0, baseUri.length - 1); // need to remove trailing / from resourceManagerEndpointUrl to correctly derive suffixes below }
} else {
let suffixKeyvault = ".vault" + baseUri.substring(baseUri.indexOf('.')); // keyvault suffix starts with . core.error(`Azure PowerShell Login failed. Please check the credentials. For more information refer https://aka.ms/create-secrets-for-GitHub-workflows"`);
let suffixStorage = baseUri.substring(baseUri.indexOf('.') + 1); // storage suffix starts without . }
let profileVersion = "2019-03-01-hybrid"; core.setFailed(error);
await executeAzCliCommand(`cloud register -n "${environment}" --endpoint-resource-manager "${resourceManagerEndpointUrl}" --suffix-keyvault-dns "${suffixKeyvault}" --suffix-storage-endpoint "${suffixStorage}" --profile "${profileVersion}"`, false); }
} finally {
catch (error) { // Reset AZURE_HTTP_USER_AGENT
core.error(`Error while trying to register cloud "${environment}": "${error}"`); core.exportVariable('AZURE_HTTP_USER_AGENT', prefix);
} core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azPSHostEnv);
}
console.log(`Done registering cloud: "${environment}"`) }
}
async function executeAzCliCommand(
await executeAzCliCommand(`cloud set -n "${environment}"`, false); command: string,
console.log(`Done setting cloud: "${environment}"`); silent?: boolean,
execOptions: any = {},
// Attempting Az cli login args: any = []) {
var commonArgs = ["--service-principal",
"-u", servicePrincipalId, execOptions.silent = !!silent;
"--tenant", tenantId try {
]; await exec.exec(`"${azPath}" ${command}`, args, execOptions);
if (allowNoSubscriptionsLogin) { }
commonArgs = commonArgs.concat("--allow-no-subscriptions"); catch (error) {
} throw new Error(error);
if (enableOIDC) { }
commonArgs = commonArgs.concat("--federated-token", 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("-p", servicePrincipalKey);
}
await executeAzCliCommand(`login`, true, loginOptions, commonArgs);
if (!allowNoSubscriptionsLogin) {
var args = [
"--subscription",
subscriptionId
];
await executeAzCliCommand(`account set`, true, loginOptions, args);
}
isAzCLISuccess = true;
if (enableAzPSSession) {
// Attempting Az PS login
console.log(`Running Azure PS Login`);
var spnlogin: ServicePrincipalLogin;
spnlogin = new ServicePrincipalLogin(
servicePrincipalId,
servicePrincipalKey,
federatedToken,
tenantId,
subscriptionId,
allowNoSubscriptionsLogin,
environment,
resourceManagerEndpointUrl);
await spnlogin.initialize();
await spnlogin.login();
}
console.log("Login successful.");
}
catch (error) {
if (!isAzCLISuccess) {
core.setFailed("Az CLI Login failed. 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. 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
core.exportVariable('AZURE_HTTP_USER_AGENT', prefix);
core.exportVariable('AZUREPS_HOST_ENVIRONMENT', azPSHostEnv);
}
}
async function executeAzCliCommand(
command: string,
silent?: boolean,
execOptions: any = {},
args: any = []) {
execOptions.silent = !!silent;
await exec.exec(`"${azPath}" ${command}`, args, execOptions);
}
async function jwtParser(federatedToken: string) {
let tokenPayload = federatedToken.split('.')[1];
let bufferObj = Buffer.from(tokenPayload, "base64");
let decodedPayload = JSON.parse(bufferObj.toString("utf8"));
return [decodedPayload['iss'], decodedPayload['sub']];
}
main(); main();