Compare commits

..

1 Commits

Author SHA1 Message Date
MoChilia
2431008d0a test update node 16 to 20 2024-02-19 11:31:25 +08:00
17 changed files with 4070 additions and 812 deletions

View File

@@ -333,3 +333,36 @@ jobs:
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@v4
- name: Set Node.js 20.x for GitHub Action
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: 'Validate build'
run: |
npm install
npm run build
- name: Login with system-assigned managed identity without auth-type
id: login_14
continue-on-error: true
uses: ./
- name: Check Last step failed
if: steps.login_14.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
core.setFailed('Last action should fail but not. Please check it.')

View File

@@ -12,7 +12,7 @@ jobs:
BasicTest:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest, macos-latest, self_linux, self_windows]
runs-on: ${{ matrix.os }}
environment: Automation test
@@ -47,11 +47,13 @@ jobs:
az vm list --output none
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = $checkResult -and ((Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG')
$checkResult = $checkResult -and ((Get-AzVM).Count -gt 0)
if(-not $checkResult){
throw "Not all checks passed!"
}
@@ -69,7 +71,7 @@ jobs:
az account show --output none
- name: Run Azure PowerShell again
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -92,11 +94,13 @@ jobs:
az vm list --output none
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = $checkResult -and ((Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG')
$checkResult = $checkResult -and ((Get-AzVM).Count -gt 0)
if(-not $checkResult){
throw "Not all checks passed!"
}
@@ -160,11 +164,13 @@ jobs:
az vm list --output none
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = $checkResult -and ((Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG')
$checkResult = $checkResult -and ((Get-AzVM).Count -gt 0)
if(-not $checkResult){
throw "Not all checks passed!"
}
@@ -183,7 +189,7 @@ jobs:
az account show --output none
- name: Run Azure PowerShell again
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -203,17 +209,17 @@ jobs:
- name: Run Azure Cli
shell: pwsh
run: |
$checkResult = (az account list --output json | ConvertFrom-Json).Count -eq 3
$checkResult = (az account list --output json | ConvertFrom-Json).Count -eq 2
if(-not $checkResult){
throw "Not all checks passed!"
}
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = (Get-AzContext -ListAvailable).Count -eq 2
if(-not $checkResult){
throw "Not all checks passed!"
}
@@ -230,7 +236,7 @@ jobs:
az account show --output none
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -239,10 +245,13 @@ jobs:
throw "Not all checks passed!"
}
InDockerTest:
runs-on: ubuntu-latest
container: ubuntu:24.04
VMTest:
strategy:
matrix:
os: [self_linux, self_windows]
runs-on: ${{ matrix.os }}
environment: Automation test
steps:
- name: 'Checking out repo code'
uses: actions/checkout@v4
@@ -251,65 +260,103 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Install Azure CLI
run: |
apt-get update
apt-get install -y curl
curl -sL https://aka.ms/InstallAzureCLIDeb | bash
- name: Check Azure CLI Version
run: |
az --version
- name: Install Powershell
run: |
apt-get update
apt-get install -y wget
wget https://ftp.debian.org/debian/pool/main/i/icu/libicu72_72.1-3_amd64.deb
dpkg -i libicu72_72.1-3_amd64.deb
wget https://github.com/PowerShell/PowerShell/releases/download/v7.5.0/powershell_7.5.0-1.deb_amd64.deb
dpkg -i powershell_7.5.0-1.deb_amd64.deb
- name: Check Powershell Version
shell: pwsh
run: |
$PSVersionTable
- name: Install Azure Powershell
shell: pwsh
run: |
Install-Module -Name Az -Repository PSGallery -Force
- name: Check Azure Powershell Version
shell: pwsh
run: |
Get-Module -ListAvailable Az
- name: 'Validate build'
run: |
npm install
npm run build
- name: 'Run L0 tests'
run: |
npm run test
- name: Login with individual parameters
- name: Login with system-assigned managed identity, no subscription-id
uses: ./
with:
client-id: ${{ secrets.SP1_CLIENT_ID }}
tenant-id: ${{ secrets.SP1_TENANT_ID }}
subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }}
auth-type: IDENTITY
allow-no-subscriptions: true
enable-AzPSSession: true
- name: Run Azure Cli again
- name: Run Azure Cli
run: |
az group list --output none
- name: Run Azure PowerShell again
uses: azure/powershell@v2
az account show --output none
- name: Run Azure PowerShell
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = Get-AzResourceGroup
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
if(-not $checkResult){
throw "Not all checks passed!"
}
- name: Login with system-assigned managed identity, with subscription id
uses: ./
with:
auth-type: IDENTITY
subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }}
enable-AzPSSession: true
- name: Run Azure Cli
run: |
az account show --output none
az group show --name GitHubAction_CI_RG --output none
az vm list --output none
- name: Run Azure PowerShell
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = $checkResult -and ((Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG')
$checkResult = $checkResult -and ((Get-AzVM).Count -gt 0)
if(-not $checkResult){
throw "Not all checks passed!"
}
- name: Login with tenant-level user-assigned managed identity with allow-no-subscriptions
uses: ./
with:
client-id: ${{ secrets.UMI2_CLIENT_ID }}
allow-no-subscriptions: true
auth-type: IDENTITY
enable-AzPSSession: true
- name: Run Azure Cli
run: |
az account show --output none
- name: Run Azure PowerShell
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
if(-not $checkResult){
throw "Not all checks passed!"
}
- name: Login with user-assigned managed identity, subscription-id
uses: ./
with:
client-id: ${{ secrets.UMI1_CLIENT_ID }}
subscription-id: ${{ secrets.UMI1_SUBSCRIPTION_ID }}
auth-type: IDENTITY
enable-AzPSSession: true
- name: Run Azure Cli
run: |
az account show --output none
az group show --name GitHubAction_CI_RG --output none
az vm list --output none
- name: Run Azure PowerShell
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
$checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud'
$checkResult = $checkResult -and ((Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG')
$checkResult = $checkResult -and ((Get-AzVM).Count -gt 0)
if(-not $checkResult){
throw "Not all checks passed!"
}

View File

@@ -1,12 +1,13 @@
name: pr-check
on:
pull_request:
pull_request_target:
branches:
- master
- 'releases/*'
jobs:
az-login-test:
environment: Automation test
runs-on: windows-latest
steps:
- name: Checkout from PR branch
@@ -27,5 +28,44 @@ jobs:
- name: Build GitHub Action
run: npm run build
- name: Run mock test
run: npm run test
- name: 'Az CLI login with subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- run: |
az account show --output none
az vm list --output none
- name: 'Az CLI login without subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS_NO_SUB }}
allow-no-subscriptions: true
- run: |
az account show --output none
# az vm list --output none
- name: 'Azure PowerShell login with subscription'
uses: ./
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
- uses: azure/powershell@v1
with:
inlineScript: "(Get-AzContext).Environment.Name"
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"

View File

@@ -29,7 +29,7 @@ jobs:
run: |
npm install
npm run build
- name: 'Run L0 tests'
run: |
npm run test

View File

@@ -6,11 +6,6 @@ on:
schedule:
- cron: '0 19 * * 0'
permissions:
actions: read
contents: read
security-events: write
jobs:
CodeQL-Build:
@@ -23,14 +18,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
languages: javascript
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -44,4 +39,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v2

View File

@@ -7,4 +7,3 @@ Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)

234
README.md
View File

@@ -19,8 +19,7 @@
- [Login to Azure US Government cloud](#login-to-azure-us-government-cloud)
- [Login to Azure Stack Hub](#login-to-azure-stack-hub)
- [Login without subscription](#login-without-subscription)
- [Enable/Disable the cleanup steps](#enabledisable-the-cleanup-steps)
- [Security hardening](#security-hardening)
- [Az logout and security hardening](#az-logout-and-security-hardening)
- [Azure CLI dependency](#azure-cli-dependency)
- [Reference](#reference)
- [GitHub Action](#github-action)
@@ -57,7 +56,7 @@ Azure Login Action supports different ways of authentication with Azure.
|tenant-id|false|UUID||the login tenant id|
|creds|false|string||a json string for login with an Azure service principal|
|enable-AzPSSession|false|boolean|false|if Azure PowerShell login is enabled|
|environment|false|string|azurecloud|the Azure Cloud environment. For cloud environments other than the public cloud, the `audience` will also need to be updated.|
|environment|false|string|azurecloud|the Azure Cloud environment|
|allow-no-subscriptions|false|boolean|false|if login without subscription is allowed|
|audience|false|string|api://AzureADTokenExchange|the audience to get the JWT ID token from GitHub OIDC provider|
|auth-type|false|string|SERVICE_PRINCIPAL|the auth type|
@@ -127,8 +126,6 @@ By default, Azure Login Action connects to the Azure Public Cloud (`AzureCloud`)
To login to one of the Azure Government clouds or Azure Stack, set `environment` to one of the supported values `AzureUSGovernment` or `AzureChinaCloud` or `AzureGermanCloud` or `AzureStack`.
The default [`audience`](#audience) for each of these clouds is different and will also need to be set if using anything other than the public environment.
Refer to [Login to Azure US Government cloud](#login-to-azure-us-government-cloud) for its usage.
### `allow-no-subscriptions`
@@ -156,7 +153,7 @@ Refer to [Login With System-assigned Managed Identity](#login-with-system-assign
> - Ensure the CLI version is 2.30 or above to support login with OIDC.
> - By default, Azure access tokens issued during OIDC based login could have limited validity. Azure access token issued by Service Principal is expected to have an expiration of 1 hour by default. And with Managed Identities, it would be 24 hours. This expiration time is further configurable in Azure. Refer to [access-token lifetime](https://learn.microsoft.com/azure/active-directory/develop/access-tokens#access-token-lifetime) for more details.
Before you use Azure Login Action with OIDC, you need to configure a federated identity credential on a service principal or a managed identity.
Before you use Azure Login Action with OIDC, you need to configure a federated identity credential on an service principal or a managed identity.
- Prepare a service principal for Login with OIDC
- [Create a service principal and assign a role to it](https://learn.microsoft.com/entra/identity-platform/howto-create-service-principal-portal)
@@ -186,21 +183,21 @@ name: Run Azure Login with OIDC
on: [push]
permissions:
id-token: write
contents: read
jobs:
id-token: write
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Azure login
uses: azure/login@v2
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
@@ -216,29 +213,29 @@ name: Run Azure Login with OIDC
on: [push]
permissions:
id-token: write
contents: read
jobs:
id-token: write
contents: read
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Azure login
uses: azure/login@v2
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: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
- name: Azure PowerShell script
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -284,17 +281,18 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
```
- **The workflow sample to run both Azure CLI and Azure PowerShell**
@@ -311,21 +309,21 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
enable-AzPSSession: true
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
- name: Azure PowerShell script
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -335,7 +333,7 @@ jobs:
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 to address the [security concerns](https://docs.github.com/actions/security-guides/encrypted-secrets), below snippet can help with the same.
```yaml
- uses: azure/login@v2
- uses: Azure/login@v1
with:
creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}'
```
@@ -374,29 +372,29 @@ Now you can try the workflow to login with system-assigned managed identity.
name: Run Azure Login with System-assigned Managed Identity
on: [push]
jobs:
jobs:
build-and-deploy:
runs-on: self-hosted
steps:
- name: Azure login
uses: azure/login@v2
uses: azure/login@v1
with:
auth-type: IDENTITY
auth-type: IDENTITY
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
# Azure CLI Action only supports linux self-hosted runners for now.
# If you want to execute the Azure CLI script on a windows self-hosted runner, you can execute it directly in `run`.
# Azure CLI Action only supports linux self-hosted runners for now.
# If you want to execute the Azure CLI script on a windows self-hosted runner, you can execute it directly in `run`.
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
- name: Azure PowerShell script
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -440,30 +438,30 @@ Now you can try the workflow to login with user-assigned managed identity.
name: Run Azure Login with User-assigned Managed Identity
on: [push]
jobs:
jobs:
build-and-deploy:
runs-on: self-hosted
steps:
- name: Azure login
uses: azure/login@v2
uses: azure/login@v1
with:
auth-type: IDENTITY
client-id: ${{ secrets.AZURE_CLIENT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
# Azure CLI Action only supports linux self-hosted runners for now.
# If you want to execute the Azure CLI script on a windows self-hosted runner, you can execute it directly in `run`.
# Azure CLI Action only supports linux self-hosted runners for now.
# If you want to execute the Azure CLI script on a windows self-hosted runner, you can execute it directly in `run`.
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
- name: Azure PowerShell script
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
@@ -484,12 +482,13 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
environment: 'AzureUSGovernment'
enable-AzPSSession: true
```
### Login to Azure Stack Hub
@@ -506,12 +505,13 @@ jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
environment: 'AzureStack'
enable-AzPSSession: true
```
Refer to the [Azure Stack Hub Login Action Tutorial](https://learn.microsoft.com/azure-stack/user/ci-cd-github-action-login-cli) for more detailed instructions.
@@ -534,7 +534,7 @@ jobs:
steps:
- name: Azure Login
uses: azure/login@v2
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -542,134 +542,44 @@ jobs:
enable-AzPSSession: true
- name: Azure CLI script
uses: azure/cli@v2
uses: azure/CLI@v1
with:
azcliversion: latest
inlineScript: |
az account show
- name: Run Azure PowerShell
uses: azure/powershell@v2
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
Get-AzContext
```
### Enable/Disable the cleanup steps
## Az logout and security hardening
In Azure Login Action, "cleanup" means cleaning up the login context. For security reasons, we recommend users run cleanup every time. But in some scenarios, users need flexible control over cleanup.
Referring to [`runs` for JavaScript actions](https://docs.github.com/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runs-for-javascript-actions), there are 3 steps in an action: `pre:`, `main:` and `post:`. Azure Login Action only implement 2 steps: `main:` and `post:`.
There are 2 "cleanup" steps in Azure Login Action:
- cleanup in `main:`
- It's **disabled** by default.
- Users can enable it by setting an env variable `AZURE_LOGIN_PRE_CLEANUP` to `true`.
- cleanup in `post:`
- It's **enabled** by default.
- Users can disable it by setting an env variable `AZURE_LOGIN_POST_CLEANUP` to `false`.
Azure Login Action use env variables to enable or disable cleanup steps. In GitHub Actions, there are three valid scopes for env variables.
- [env](https://docs.github.com/actions/writing-workflows/workflow-syntax-for-github-actions#env)
- valid for all jobs in this workflow.
- [jobs.<job_id>.env](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idenv)
- valid for all the steps in the job.
- [jobs.<job_id>.steps[*].env](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsenv)
- only valid for the step in a job.
We set `jobs.<job_id>.steps[*].env` for example. Users can set `env` or `jobs.<job_id>.env` for a wider scope.
```yaml
# File: .github/workflows/workflow.yml
on: [push]
name: Cleanup examples for Multiple Azure Login
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# enable cleanup for the 1st Azure Login
- name: Azure Login
uses: azure/login@v2
env:
AZURE_LOGIN_PRE_CLEANUP: true
AZURE_LOGIN_POST_CLEANUP: true
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
# run some actions
# disable cleanup for all other Azure Login
- name: Azure Login 2
uses: azure/login@v2
env:
AZURE_LOGIN_PRE_CLEANUP: false
AZURE_LOGIN_POST_CLEANUP: false
with:
client-id: ${{ secrets.AZURE_CLIENT_ID_2 }}
tenant-id: ${{ secrets.AZURE_TENANT_ID_2 }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_2 }}
enable-AzPSSession: true
# run other actions
# disable cleanup for all other Azure Login
- name: Azure Login 3
uses: azure/login@v2
env:
AZURE_LOGIN_PRE_CLEANUP: false
AZURE_LOGIN_POST_CLEANUP: false
with:
client-id: ${{ secrets.AZURE_CLIENT_ID_3 }}
tenant-id: ${{ secrets.AZURE_TENANT_ID_3 }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID_3 }}
enable-AzPSSession: true
# run other actions
```
```yaml
# File: .github/workflows/workflow.yml
on: [push]
name: Disable cleanup for GitHub Hosted Runners
jobs:
deploy:
runs-on: [ubuntu-latest, self-hosted]
steps:
- name: Azure Login
uses: azure/login@v2
env:
AZURE_LOGIN_PRE_CLEANUP: ${{ startsWith(runner.name, 'GitHub Actions') }}
AZURE_LOGIN_POST_CLEANUP: ${{ startsWith(runner.name, 'GitHub Actions') }}
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
enable-AzPSSession: true
# run some actions
```
## Security hardening
This action doesn't implement ```az logout``` by default at the end of execution. However, there is no way to tamper with the credentials or account information because the GitHub-hosted runner is on a VM that will get re-imaged for every customer run, which deletes everything. But if the runner is self-hosted (not provided by GitHub), it is recommended to manually log out at the end of the workflow, as shown below. More details on security of the runners can be found [here](https://docs.github.com/actions/learn-github-actions/security-hardening-for-github-actions#hardening-for-self-hosted-runners).
> [!WARNING]
> When using self hosted runners it is possible to have multiple runners on a single VM. Currently if your runners share a single user on the VM each runner will share the same credentials. That means in detail that each runner is able to change the permissions of another run. As a workaround we propose to use one single VM user per runner. If you start the runner as a service, do not forget to add the [optional user argument](https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service#installing-the-service)
> When using self hosted runners it is possible to have multiple runners on a single VM. Currently if your runners share a single user on the VM each runner will share the same credentials. That means in detail that each runner is able to change the permissions of another run. As a workaround we propose to use one single VM user per runner. If you start the runner as a service, do not forget to add the [optional user argument](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service#installing-the-service)
```yaml
- name: Azure CLI script
uses: azure/CLI@v1
with:
inlineScript: |
az logout
az cache purge
az account clear
- name: Azure PowerShell script
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
Clear-AzContext -Scope Process
Clear-AzContext -Scope CurrentUser
```
## Azure CLI dependency
@@ -679,7 +589,7 @@ Internally in this action, we use azure CLI and execute `az login` with the cred
### GitHub Action
[GitHub Actions](https://docs.github.com/actions) gives you the flexibility to build an automated software development lifecycle workflow.
[GitHub Actions](https://help.github.com/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow.
### GitHub Actions for deploying to Azure

View File

@@ -96,7 +96,7 @@ describe("LoginConfig Test", () => {
expect(loginConfig.servicePrincipalId).toBe("client-id");
expect(loginConfig.servicePrincipalSecret).toBe("client-secret");
expect(loginConfig.tenantId).toBe("tenant-id");
expect(loginConfig.subscriptionId).toBe(undefined);
expect(loginConfig.subscriptionId).toBe("");
});
test('initialize with creds', async () => {
@@ -245,7 +245,7 @@ describe("LoginConfig Test", () => {
let loginConfig = new LoginConfig();
await loginConfig.initialize();
testValidateWithErrorMessage(loginConfig, "Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'.");
testValidateWithErrorMessage(loginConfig, "Ensure subscriptionId is supplied.");
});
test('validate without subscriptionId and allowNoSubscriptionsLogin=true', async () => {

View File

@@ -40,7 +40,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy();
expect(loginMethod).toBe('service principal with secret');
});
});
@@ -61,7 +61,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-se''cret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-se''cret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy();
expect(loginMethod).toBe('service principal with secret');
});
});
@@ -82,7 +82,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("$psLoginSecrets = ConvertTo-SecureString 'client-secret' -AsPlainText -Force; $psLoginCredential = New-Object System.Management.Automation.PSCredential('client-id', $psLoginSecrets); Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -Credential $psLoginCredential | out-null;")).toBeTruthy();
expect(loginMethod).toBe('service principal with secret');
});
});
@@ -100,7 +100,7 @@ describe("Getting AzLogin PS script", () => {
loginConfig.initialize();
jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => {loginConfig.federatedToken = "fake-token";});
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -ApplicationId 'client-id' -FederatedToken 'fake-token' -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -ApplicationId 'client-id' -FederatedToken 'fake-token' | out-null;")).toBeTruthy();
expect(loginMethod).toBe('OIDC');
});
});
@@ -115,7 +115,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -Subscription 'subscription-id' -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -Subscription 'subscription-id' | out-null;")).toBeTruthy();
expect(loginMethod).toBe('system-assigned managed identity');
});
});
@@ -130,7 +130,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' | out-null;")).toBeTruthy();
expect(loginMethod).toBe('system-assigned managed identity');
});
});
@@ -145,7 +145,7 @@ describe("Getting AzLogin PS script", () => {
let loginConfig = new LoginConfig();
loginConfig.initialize();
return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => {
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -AccountId 'client-id' -InformationAction Ignore | out-null;")).toBeTruthy();
expect(loginScript.includes("Connect-AzAccount -Identity -Environment 'azurecloud' -AccountId 'client-id' | out-null;")).toBeTruthy();
expect(loginMethod).toBe('user-assigned managed identity');
});
});

View File

@@ -1,7 +1,7 @@
# Login to Azure subscription
name: 'Azure Login'
description: 'Authenticate to Azure and run your Azure CLI or Azure PowerShell based actions or scripts.'
inputs:
inputs:
creds:
description: 'Paste output of `az ad sp create-for-rbac` as value of secret variable: AZURE_CREDENTIALS'
required: false
@@ -14,7 +14,7 @@ inputs:
subscription-id:
description: 'Azure subscriptionId'
required: false
enable-AzPSSession:
enable-AzPSSession:
description: 'Set this value to true to enable Azure PowerShell Login in addition to Azure CLI login'
required: false
default: false
@@ -27,11 +27,11 @@ inputs:
required: false
default: false
audience:
description: 'Provide audience field for access-token. Default value is api://AzureADTokenExchange'
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'
description: 'The type of authentication. Supported values are SERVICE_PRINCIPAL, IDENTITY. Default value is SERVICE_PRINCIPAL'
required: false
default: 'SERVICE_PRINCIPAL'
branding:
@@ -39,6 +39,6 @@ branding:
color: 'blue'
runs:
using: 'node20'
main: 'lib/main/index.js'
post-if: (!env.AZURE_LOGIN_POST_CLEANUP || env.AZURE_LOGIN_POST_CLEANUP != 'false')
post: 'lib/cleanup/index.js'
pre: 'lib/cleanup.js'
main: 'lib/main.js'
post: 'lib/cleanup.js'

4242
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,17 @@
{
"name": "login",
"version": "2.2.0",
"version": "1.0.0",
"description": "Login Azure wraps the az login, allowing for Azure actions to log into Azure",
"main": "lib/main/index.js",
"main": "lib/main.js",
"scripts": {
"build:main": "ncc build src/main.ts -o lib/main",
"build:cleanup": "ncc build src/cleanup.ts -o lib/cleanup",
"build": "npm run build:main && npm run build:cleanup",
"build": "tsc",
"test": "jest"
},
"author": "Microsoft",
"author": "Sumiran Aggarwal",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.2.4",
"@types/node": "^20.11.1",
"@vercel/ncc": "^0.38.1",
"@types/node": "^12.7.11",
"jest": "^29.3.1",
"jest-circus": "^29.3.1",
"ts-jest": "^29.0.3",
@@ -24,6 +21,7 @@
"@actions/core": "1.9.1",
"@actions/exec": "^1.0.1",
"@actions/io": "^1.0.1",
"actions-secret-parser": "^1.0.4",
"package-lock": "^1.0.3"
}
}
}

View File

@@ -8,7 +8,6 @@ export class AzureCliLogin {
loginConfig: LoginConfig;
azPath: string;
loginOptions: ExecOptions;
azVersion: string;
constructor(loginConfig: LoginConfig) {
this.loginConfig = loginConfig;
@@ -29,15 +28,10 @@ export class AzureCliLogin {
}
};
await this.executeAzCliCommand(["version"], true, execOptions);
await this.executeAzCliCommand(["--version"], true, execOptions);
core.debug(`Azure CLI version used:\n${output}`);
try {
this.azVersion = JSON.parse(output)["azure-cli"];
}
catch (error) {
core.warning("Failed to parse Azure CLI version.");
}
await this.registerAzurestackEnvIfNecessary();
this.setAzurestackEnvIfNecessary();
await this.executeAzCliCommand(["cloud", "set", "-n", this.loginConfig.environment], false);
core.info(`Done setting cloud: "${this.loginConfig.environment}"`);
@@ -65,7 +59,7 @@ export class AzureCliLogin {
}
}
async registerAzurestackEnvIfNecessary() {
async setAzurestackEnvIfNecessary() {
if (this.loginConfig.environment != "azurestack") {
return;
}
@@ -91,7 +85,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}"`);
@@ -114,20 +108,7 @@ export class AzureCliLogin {
}
async loginWithUserAssignedIdentity(args: string[]) {
let azcliMinorVersion = 0;
try {
azcliMinorVersion = parseInt(this.azVersion.split('.')[1], 10);
}
catch (error) {
core.warning("Failed to parse the minor version of Azure CLI. Assuming the version is less than 2.69.0");
}
//From Azure-cli v2.69.0, `--username` is replaced with `--client-id`, `--object-id` or `--resource-id`: https://github.com/Azure/azure-cli/pull/30525
if (azcliMinorVersion < 69) {
args.push("--username", this.loginConfig.servicePrincipalId);
}
else {
args.push("--client-id", this.loginConfig.servicePrincipalId);
}
args.push("--username", this.loginConfig.servicePrincipalId);
await this.callCliLogin(args, 'user-assigned managed identity');
}

View File

@@ -104,7 +104,7 @@ export default class AzPSScriptBuilder {
if(subscriptionId){
loginCmdlet += `-Subscription '${subscriptionId}' `;
}
loginCmdlet += `${cmdletSuffix} -InformationAction Ignore | out-null;`;
loginCmdlet += `${cmdletSuffix} | out-null;`;
return loginCmdlet;
}
}

View File

@@ -1,4 +1,5 @@
import * as core from '@actions/core';
import { FormatType, SecretParser } from 'actions-secret-parser';
export class LoginConfig {
static readonly AUTH_TYPE_SERVICE_PRINCIPAL = "SERVICE_PRINCIPAL";
@@ -48,10 +49,10 @@ export class LoginConfig {
private readParametersFromCreds() {
let creds = core.getInput('creds', { required: false });
if (!creds) {
let secrets = creds ? new SecretParser(creds, FormatType.JSON) : null;
if (!secrets) {
return;
}
let secrets = JSON.parse(creds);
if(this.authType != LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL){
return;
@@ -63,11 +64,11 @@ export class LoginConfig {
}
core.debug('Reading creds in JSON...');
this.servicePrincipalId = this.servicePrincipalId ? this.servicePrincipalId : secrets.clientId;
this.servicePrincipalSecret = secrets.clientSecret;
this.tenantId = this.tenantId ? this.tenantId : secrets.tenantId;
this.subscriptionId = this.subscriptionId ? this.subscriptionId : secrets.subscriptionId;
this.resourceManagerEndpointUrl = secrets.resourceManagerEndpointUrl;
this.servicePrincipalId = this.servicePrincipalId ? this.servicePrincipalId : secrets.getSecret("$.clientId", false);
this.servicePrincipalSecret = secrets.getSecret("$.clientSecret", false);
this.tenantId = this.tenantId ? this.tenantId : secrets.getSecret("$.tenantId", false);
this.subscriptionId = this.subscriptionId ? this.subscriptionId : secrets.getSecret("$.subscriptionId", false);
this.resourceManagerEndpointUrl = secrets.getSecret("$.resourceManagerEndpointUrl", false);
if (!this.servicePrincipalId || !this.servicePrincipalSecret || !this.tenantId) {
throw new Error("Not all parameters are provided in 'creds'. Double-check if all keys are defined in 'creds': 'clientId', 'clientSecret', 'tenantId'.");
}
@@ -79,16 +80,11 @@ export class LoginConfig {
this.mask(this.federatedToken);
}
catch (error) {
core.error("Failed to fetch federated token from GitHub. 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;
}
try {
let [issuer, subjectClaim, audience, jobWorkflowRef] = await jwtParser(this.federatedToken);
core.info("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim + "\n audience - " + audience + "\n job_workflow_ref - " + jobWorkflowRef);
}
catch (error) {
core.warning(`Failed to parse the federated token. Error: ${error}`);
}
let [issuer, subjectClaim] = await jwtParser(this.federatedToken);
core.info("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim);
}
validate() {
@@ -104,7 +100,7 @@ export class LoginConfig {
}
}
if (!this.subscriptionId && !this.allowNoSubscriptionsLogin) {
throw new Error("Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'.");
throw new Error("Ensure subscriptionId is supplied.");
}
}
@@ -119,20 +115,6 @@ async function jwtParser(federatedToken: string) {
let tokenPayload = federatedToken.split('.')[1];
let bufferObj = Buffer.from(tokenPayload, "base64");
let decodedPayload = JSON.parse(bufferObj.toString("utf8"));
const JWT_CLAIM_ISSUER = 'iss';
const JWT_CLAIM_SUBJECT = 'sub';
const JWT_CLAIM_AUDIENCE = 'aud';
const JWT_CLAIM_JOB_WORKFLOW_REF = 'job_workflow_ref';
const requiredClaims = [
JWT_CLAIM_ISSUER,
JWT_CLAIM_SUBJECT,
JWT_CLAIM_AUDIENCE,
JWT_CLAIM_JOB_WORKFLOW_REF
];
for (const claim of requiredClaims) {
if (!decodedPayload[claim]) {
throw new Error(`The claim '${claim}' is missing from the token payload`);
}
}
return [decodedPayload[JWT_CLAIM_ISSUER], decodedPayload[JWT_CLAIM_SUBJECT], decodedPayload[JWT_CLAIM_AUDIENCE], decodedPayload[JWT_CLAIM_JOB_WORKFLOW_REF]];
}
return [decodedPayload['iss'], decodedPayload['sub']];
}

View File

@@ -7,8 +7,8 @@ import { AzPSConstants, AzPSUtils } from '../PowerShell/AzPSUtils';
export function setUserAgent(): void {
let usrAgentRepo = crypto.createHash('sha256').update(`${process.env.GITHUB_REPOSITORY}`).digest('hex');
let actionName = 'AzureLogin';
process.env.AZURE_HTTP_USER_AGENT = (!!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT} ` : '') + `GITHUBACTIONS/${actionName}@v2_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
process.env.AZUREPS_HOST_ENVIRONMENT = (!!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT} ` : '') + `GITHUBACTIONS/${actionName}@v2_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`;
process.env.AZURE_HTTP_USER_AGENT = (!!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT} ` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
process.env.AZUREPS_HOST_ENVIRONMENT = (!!process.env.AZUREPS_HOST_ENVIRONMENT ? `${process.env.AZUREPS_HOST_ENVIRONMENT} ` : '') + `GITHUBACTIONS/${actionName}@v1_${usrAgentRepo}`;
}
export async function cleanupAzCLIAccounts(): Promise<void> {

View File

@@ -1,5 +1,5 @@
import * as core from '@actions/core';
import { cleanupAzCLIAccounts, cleanupAzPSAccounts, setUserAgent } from './common/Utils';
import { setUserAgent } from './common/Utils';
import { AzPSLogin } from './PowerShell/AzPSLogin';
import { LoginConfig } from './common/LoginConfig';
import { AzureCliLogin } from './Cli/AzureCliLogin';
@@ -7,13 +7,6 @@ import { AzureCliLogin } from './Cli/AzureCliLogin';
async function main() {
try {
setUserAgent();
const preCleanup: string = process.env.AZURE_LOGIN_PRE_CLEANUP;
if ('true' == preCleanup) {
await cleanupAzCLIAccounts();
if (core.getInput('enable-AzPSSession').toLowerCase() === "true") {
await cleanupAzPSAccounts();
}
}
// prepare the login configuration
var loginConfig = new LoginConfig();