diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 27a83c107..f9bb67a67 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -@kaverma @kanika1894 @BALAGA-GAYATRI @pulkitaggarwl +@Azure/act-identity-squad diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..f0028db08 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,36 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + labels: ["dependencies"] + groups: + dev-dependencies: + dependency-type: "development" + update-types: ["minor", "patch"] + ignore: + # ts-jest currently supports "typescript >=4.3 <7", so cap TypeScript + # below 7 (the native compiler rewrite) until ts-jest supports it. + - dependency-name: "typescript" + versions: [">=7"] + # @actions/io, @actions/exec and @actions/core 3.x are ESM-only + # (package.json "type": "module"), which the CommonJS ncc bundle cannot + # require(). @actions/http-client 3.x is still CommonJS, so it is not + # capped. Stay on 2.x for the ESM packages until the action migrates to ESM. + - dependency-name: "@actions/io" + versions: [">=3"] + - dependency-name: "@actions/exec" + versions: [">=3"] + - dependency-name: "@actions/core" + versions: [">=3"] + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: ["*"] diff --git a/.github/workflows/azure-login-canary.yml b/.github/workflows/azure-login-canary.yml index 5c430f703..8978ff008 100644 --- a/.github/workflows/azure-login-canary.yml +++ b/.github/workflows/azure-login-canary.yml @@ -30,10 +30,10 @@ jobs: az --version - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: 'Az CLI login with subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} @@ -41,7 +41,7 @@ jobs: az account show --output none - name: 'Az CLI login without subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} allow-no-subscriptions: true @@ -50,7 +50,7 @@ jobs: az account show --output none - name: 'Az CLI login with subscription OIDC' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} @@ -60,7 +60,7 @@ jobs: az account show --output none - name: 'Az CLI login without subscription OIDC' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} @@ -68,25 +68,3 @@ jobs: - run: | az account show --output none - - 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 "report=$REPORT" >> $GITHUB_OUTPUT - - 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}} - diff --git a/.github/workflows/azure-login-integration-tests.yml b/.github/workflows/azure-login-integration-tests.yml index 120b5a71b..31b91186e 100644 --- a/.github/workflows/azure-login-integration-tests.yml +++ b/.github/workflows/azure-login-integration-tests.yml @@ -14,7 +14,7 @@ jobs: # continue-on-error: true steps: - name: 'Az CLI login with subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} @@ -23,7 +23,7 @@ jobs: az vm list --output none - name: 'Az CLI login without subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} allow-no-subscriptions: true @@ -32,24 +32,24 @@ jobs: az account show --output none - name: 'Azure PowerShell login with subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} enable-AzPSSession: true - - uses: azure/powershell@v1 + - uses: azure/powershell@v3 with: inlineScript: "(Get-AzContext).Environment.Name" azPSVersion: "latest" - name: 'Azure PowerShell login without subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: creds: ${{secrets.AZURE_CREDENTIALS}} enable-AzPSSession: true allow-no-subscriptions: true - - uses: azure/powershell@v1 + - uses: azure/powershell@v3 with: inlineScript: "(Get-AzContext).Environment.Name" azPSVersion: "latest" @@ -59,7 +59,7 @@ jobs: # continue-on-error: true steps: - name: 'Az CLI login with subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} @@ -70,7 +70,7 @@ jobs: az vm list --output none - name: 'Az CLI login without subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} @@ -80,50 +80,27 @@ jobs: az account show --output none - name: 'Azure PowerShell login with subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTIONID }} enable-AzPSSession: true - - uses: azure/powershell@v1 + - uses: azure/powershell@v3 with: inlineScript: "(Get-AzContext).Environment.Name" azPSVersion: "latest" - name: 'Azure PowerShell login without subscription' - uses: azure/login@v1 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENTID }} tenant-id: ${{ secrets.AZURE_TENANTID }} enable-AzPSSession: true allow-no-subscriptions: true - - uses: azure/powershell@v1 + - uses: azure/powershell@v3 with: inlineScript: "(Get-AzContext).Environment.Name" 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 "report=$REPORT" >> $GITHUB_OUTPUT - - - 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}} diff --git a/.github/workflows/azure-login-live-tests.yml b/.github/workflows/azure-login-live-tests.yml new file mode 100644 index 000000000..0e2aeffad --- /dev/null +++ b/.github/workflows/azure-login-live-tests.yml @@ -0,0 +1,693 @@ +name: Azure Login Action Live Tests +on: + workflow_dispatch: + pull_request: + branches: + - master + +permissions: + id-token: write + contents: read + +# Each run provisions its own uniquely-named resource group (suffixed with the +# run id), so concurrent runs on different branches/PRs never collide or tear +# down each other's group. The concurrency group is branch-scoped only to avoid +# piling up runs on rapid pushes to the same ref - it does NOT serialize across +# the whole repo, so an unapproved run on one PR can never block another. +concurrency: + group: azure-login-live-tests-${{ github.ref }} + cancel-in-progress: false + +env: + # Created once in setup; the positive and embedded "can access" assertions + # target this group. Suffixed with the run id so every run is isolated. + RG_POSITIVE: GitHubAction_CI_Group_${{ github.run_id }} + LOCATION: eastus + # Intentionally NEVER created: the negative permission/not-found assertions + # target this name so they fail as expected. + RG_NEGATIVE: GitHubAction_CI_RG + +jobs: + # ---------------------------------------------------------------- setup + # Provision the resource group the "can access" assertions target. Uses a + # repo-level provisioning credential (PROVISION_CREDS) and does NOT declare an + # environment, so it is not subject to the Automation test approval gate - + # setup/teardown are provisioning, not part of the gated test surface. + setup: + runs-on: ubuntu-latest + steps: + - name: Azure login (provisioning) + uses: azure/login@v3 + with: + creds: ${{ secrets.PROVISION_CREDS }} + - name: Create resource group + run: az group create --name "${{ env.RG_POSITIVE }}" --location "${{ env.LOCATION }}" --output none + + # ================================================================ POSITIVE + Positive_BasicTest: + needs: setup + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + - name: 'Checking out repo code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: 'Run L0 tests' + run: | + npm run test + + - name: Login with creds + uses: ./ + with: + creds: ${{secrets.SP1}} + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Run Azure PowerShell + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + - name: Login with individual parameters + uses: ./ + with: + client-id: ${{ secrets.SP1_CLIENT_ID }} + tenant-id: ${{ secrets.SP1_TENANT_ID }} + subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Run Azure Cli again + run: | + az account show --output none + + - name: Run Azure PowerShell again + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + - name: Login with explicit auth-type + uses: ./ + with: + creds: ${{secrets.SP1}} + auth-type: SERVICE_PRINCIPAL + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Run Azure PowerShell + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + Positive_ParameterTest: + needs: setup + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + - name: 'Checking out repo code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: Login with creds, disable ps session + uses: ./ + with: + creds: ${{secrets.SP1}} + enable-AzPSSession: false + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Login with creds, wrong boolean value + uses: ./ + with: + creds: ${{secrets.SP1}} + enable-AzPSSession: notboolean + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Login with creds, allow no subscription + uses: ./ + with: + creds: ${{secrets.SP1}} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Run Azure PowerShell + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + - name: Login with individual parameters, allow no subscription + uses: ./ + with: + client-id: ${{ secrets.SP1_CLIENT_ID }} + tenant-id: ${{ secrets.SP1_TENANT_ID}} + subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli again + run: | + az account show --output none + + - name: Run Azure PowerShell again + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + - name: Login with individual parameters, no subscription, allow no subscription + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli + shell: pwsh + run: | + $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@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + - name: Login with creds, no subscription, allow no subscription + uses: ./ + with: + creds: '{"clientId":"${{ secrets.OIDC_SP2_CLIENT_ID }}","clientSecret":"${{ secrets.SP2_CLIENT_SECRET }}","tenantId":"${{ secrets.OIDC_SP2_TENANT_ID }}"}' + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli + run: | + az account show --output none + + - name: Run Azure PowerShell + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' + if(-not $checkResult){ + throw "Not all checks passed!" + } + + Positive_InDockerTest: + needs: setup + runs-on: ubuntu-latest + container: ubuntu:24.04 + environment: Automation test + steps: + - name: 'Checking out repo code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.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 apt-transport-https software-properties-common + wget -q "https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb" + dpkg -i packages-microsoft-prod.deb + rm packages-microsoft-prod.deb + apt-get update + apt-get install -y powershell + + - 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 + uses: ./ + with: + client-id: ${{ secrets.SP1_CLIENT_ID }} + tenant-id: ${{ secrets.SP1_TENANT_ID }} + subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Run Azure Cli again + run: | + az group list --output none + + - name: Run Azure PowerShell again + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + $checkResult = Get-AzResourceGroup + + # ================================================================ NEGATIVE + Negative_PermissionTest: + needs: setup + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + + - name: 'Checking out repo code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: Login with individual parameters + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + # subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Run Azure Cli + id: cli_3 + continue-on-error: true + run: | + az account show --output none + az group show --name GitHubAction_CI_RG --output none + + - name: Check Last step failed + if: steps.cli_3.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Run Azure PowerShell + id: ps_3 + continue-on-error: true + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + (Get-AzContext).Environment.Name -eq 'AzureCloud' + (Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG' + + - name: Check Last step failed + if: steps.ps_3.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + Negative_ParameterTest: + needs: setup + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + environment: Automation test + + steps: + - name: 'Checking out repo code' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.x + + - name: 'Validate build' + run: | + npm install + npm run build + + - name: Login with creds, missing parameters in creds + id: login_4 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP3_NO_Secret}} + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_4.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with creds, wrong keys + id: login_5 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP4_Wrong_Key}} + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_5.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with creds, no creds or individual parameters + id: login_6 + continue-on-error: true + uses: ./ + with: + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_6.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with individual parameters, only client-id, no tenant-id, subscription-id + id: login_7 + continue-on-error: true + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_7.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with individual parameters, only tenant-id, subscription-id, no client-id + id: login_8 + continue-on-error: true + uses: ./ + with: + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_8.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with creds, disable ps session + uses: ./ + with: + creds: ${{secrets.SP1}} + enable-AzPSSession: false + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Run Azure PowerShell + id: ps_8 + continue-on-error: true + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + (Get-AzContext).Environment.Name -eq 'AzureCloud' + (Get-AzResourceGroup -Name $env:RG_POSITIVE).ResourceGroupName -eq $env:RG_POSITIVE + + - name: Check Last step failed + if: steps.ps_8.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with creds, wrong boolean value + uses: ./ + with: + creds: ${{secrets.SP1}} + enable-AzPSSession: notboolean + + - name: Run Azure Cli + run: | + az account show --output none + az group show --name "${{ env.RG_POSITIVE }}" --output none + + - name: Run Azure PowerShell + id: ps_9 + continue-on-error: true + uses: azure/powershell@v3 + with: + azPSVersion: "latest" + inlineScript: | + (Get-AzContext).Environment.Name -eq 'AzureCloud' + (Get-AzResourceGroup -Name $env:RG_POSITIVE).ResourceGroupName -eq $env:RG_POSITIVE + + - name: Check Last step failed + if: steps.ps_9.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with individual parameters, with a wrong audience + id: login_10 + continue-on-error: true + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + audience: "https://github.com/actions" + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_10.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with tenant-level account, without allow-no-subscriptions + id: login_11 + continue-on-error: true + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_11.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + # SP1 is ignored and SP2 will be used for login, but it will fail since SP2 has no access to the given subscription + - name: Login with both creds and individual parameters + id: login_12 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP1}} + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_12.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login by OIDC with all info in creds + id: login_13 + continue-on-error: true + uses: ./ + with: + creds: ${{secrets.SP2}} + allow-no-subscriptions: true + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_13.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with individual parameters, no subscription-id, no allow-no-subscriptions + id: login_14 + continue-on-error: true + uses: ./ + with: + client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} + tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_14.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + - name: Login with creds, no subscription-id, no allow-no-subscriptions + id: login_15 + continue-on-error: true + uses: ./ + with: + creds: '{"clientId":"${{ secrets.OIDC_SP2_CLIENT_ID }}","clientSecret":"${{ secrets.SP2_CLIENT_SECRET }}","tenantId":"${{ secrets.OIDC_SP2_TENANT_ID }}"}' + enable-AzPSSession: true + + - name: Check Last step failed + if: steps.login_15.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + core.setFailed('Last action should fail but not. Please check it.') + + # ---------------------------------------------------------------- teardown + # Always delete the resource group created in setup, even if gate jobs failed, + # so nothing is left running (cost) between the infrequent runs. Runs + # synchronously (no --no-wait) so the group is fully gone before the run ends + # and the concurrency lock releases, preventing a delete from a finished run + # racing the setup of the next one. + teardown: + needs: [setup, Positive_BasicTest, Positive_ParameterTest, Positive_InDockerTest, Negative_PermissionTest, Negative_ParameterTest] + if: always() + runs-on: ubuntu-latest + steps: + - name: Azure login (provisioning) + uses: azure/login@v3 + with: + creds: ${{ secrets.PROVISION_CREDS }} + - name: Delete resource group + run: az group delete --name "${{ env.RG_POSITIVE }}" --yes --output none || true diff --git a/.github/workflows/azure-login-negative.yml b/.github/workflows/azure-login-negative.yml deleted file mode 100644 index 91dda5801..000000000 --- a/.github/workflows/azure-login-negative.yml +++ /dev/null @@ -1,335 +0,0 @@ -name: Azure Login Action Negative Test -on: - workflow_dispatch: - push: - -permissions: - id-token: write - contents: read - -jobs: - - PermissionTest: - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - 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 individual parameters - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - # subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Run Azure Cli - id: cli_3 - continue-on-error: true - run: | - az account show --output none - az group show --name GitHubAction_CI_RG --output none - az vm list --output none - - - name: Check Last step failed - if: steps.cli_3.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Run Azure PowerShell - id: ps_3 - continue-on-error: true - uses: azure/powershell@v1 - with: - azPSVersion: "latest" - inlineScript: | - (Get-AzContext).Environment.Name -eq 'AzureCloud' - (Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG' - (Get-AzVM).Count -gt 0 - - - name: Check Last step failed - if: steps.ps_3.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - ParameterTest: - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - 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 creds, missing parameters in creds - id: login_4 - continue-on-error: true - uses: ./ - with: - creds: ${{secrets.SP3_NO_Secret}} - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_4.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with creds, wrong keys - id: login_5 - continue-on-error: true - uses: ./ - with: - creds: ${{secrets.SP4_Wrong_Key}} - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_5.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with creds, no creds or individual parameters - id: login_6 - continue-on-error: true - uses: ./ - with: - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_6.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with individual parameters, only client-id, no tenant-id, subscription-id - id: login_7 - continue-on-error: true - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_7.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with individual parameters, only tenant-id, subscription-id, no client-id - id: login_8 - continue-on-error: true - uses: ./ - with: - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_8.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with creds, disable ps session - uses: ./ - with: - creds: ${{secrets.SP1}} - enable-AzPSSession: false - - - 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 - id: ps_8 - continue-on-error: true - uses: azure/powershell@v1 - with: - azPSVersion: "latest" - inlineScript: | - (Get-AzContext).Environment.Name -eq 'AzureCloud' - (Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG' - (Get-AzVM).Count -gt 0 - - - name: Check Last step failed - if: steps.ps_8.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with creds, wrong boolean value - uses: ./ - with: - creds: ${{secrets.SP1}} - enable-AzPSSession: notboolean - - - 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 - id: ps_9 - continue-on-error: true - uses: azure/powershell@v1 - with: - azPSVersion: "latest" - inlineScript: | - (Get-AzContext).Environment.Name -eq 'AzureCloud' - (Get-AzResourceGroup -Name GitHubAction_CI_RG).ResourceGroupName -eq 'GitHubAction_CI_RG' - (Get-AzVM).Count -gt 0 - - - name: Check Last step failed - if: steps.ps_9.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with individual parameters, with a wrong audience - id: login_10 - continue-on-error: true - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - audience: "https://github.com/actions" - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_10.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with tenant-level account, without allow-no-subscriptions - id: login_11 - continue-on-error: true - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_11.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - # SP1 is ignored and SP2 will be used for login, but it will fail since SP2 has no access to the given subscription - - name: Login with both creds and individual parameters - id: login_12 - continue-on-error: true - uses: ./ - with: - creds: ${{secrets.SP1}} - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - subscription-id: ${{ secrets.OIDC_SP2_SUBSCRIPTION_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_12.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login by OIDC with all info in creds - id: login_13 - continue-on-error: true - uses: ./ - with: - creds: ${{secrets.SP2}} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_13.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') - - - name: Login with individual parameters, no subscription-id, no allow-no-subscriptions - id: login_14 - continue-on-error: true - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - enable-AzPSSession: true - - - 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.') - - - name: Login with creds, no subscription-id, no allow-no-subscriptions - id: login_15 - continue-on-error: true - uses: ./ - with: - creds: '{"clientId":"${{ secrets.OIDC_SP2_CLIENT_ID }}","clientSecret":"${{ secrets.SP2_CLIENT_SECRET }}","tenantId":"${{ secrets.OIDC_SP2_TENANT_ID }}"}' - enable-AzPSSession: true - - - name: Check Last step failed - if: steps.login_15.outcome == 'success' - uses: actions/github-script@v7 - with: - script: | - core.setFailed('Last action should fail but not. Please check it.') diff --git a/.github/workflows/azure-login-positive.yml b/.github/workflows/azure-login-positive.yml deleted file mode 100644 index 351f230c0..000000000 --- a/.github/workflows/azure-login-positive.yml +++ /dev/null @@ -1,315 +0,0 @@ -name: Azure Login Action Positive Test -on: - workflow_dispatch: - push: - -permissions: - id-token: write - contents: read - -jobs: - - BasicTest: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - 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: 'Run L0 tests' - run: | - npm run test - - - name: Login with creds - uses: ./ - with: - creds: ${{secrets.SP1}} - 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@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Login with individual parameters - uses: ./ - with: - client-id: ${{ secrets.SP1_CLIENT_ID }} - tenant-id: ${{ secrets.SP1_TENANT_ID }} - subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - - name: Run Azure Cli again - run: | - az account show --output none - - - name: Run Azure PowerShell again - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Login with explicit auth-type - uses: ./ - with: - creds: ${{secrets.SP1}} - auth-type: SERVICE_PRINCIPAL - enable-AzPSSession: true - - - name: Run Azure Cli - run: | - az account show --output none - az group show --name GitHubAction_CI_RG --output none - az vm list --output none - - - name: Run Azure PowerShell - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - ParameterTest: - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - 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 creds, disable ps session - uses: ./ - with: - creds: ${{secrets.SP1}} - enable-AzPSSession: false - - - 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: Login with creds, wrong boolean value - uses: ./ - with: - creds: ${{secrets.SP1}} - enable-AzPSSession: notboolean - - - 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: Login with creds, allow no subscription - uses: ./ - with: - creds: ${{secrets.SP1}} - allow-no-subscriptions: true - 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@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Login with individual parameters, allow no subscription - uses: ./ - with: - client-id: ${{ secrets.SP1_CLIENT_ID }} - tenant-id: ${{ secrets.SP1_TENANT_ID}} - subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Run Azure Cli again - run: | - az account show --output none - - - name: Run Azure PowerShell again - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Login with individual parameters, no subscription, allow no subscription - uses: ./ - with: - client-id: ${{ secrets.OIDC_SP2_CLIENT_ID }} - tenant-id: ${{ secrets.OIDC_SP2_TENANT_ID }} - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Run Azure Cli - shell: pwsh - run: | - $checkResult = (az account list --output json | ConvertFrom-Json).Count -eq 3 - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Run Azure PowerShell - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - - name: Login with creds, no subscription, allow no subscription - uses: ./ - with: - creds: '{"clientId":"${{ secrets.OIDC_SP2_CLIENT_ID }}","clientSecret":"${{ secrets.SP2_CLIENT_SECRET }}","tenantId":"${{ secrets.OIDC_SP2_TENANT_ID }}"}' - allow-no-subscriptions: true - enable-AzPSSession: true - - - name: Run Azure Cli - run: | - az account show --output none - - - name: Run Azure PowerShell - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = (Get-AzContext).Environment.Name -eq 'AzureCloud' - if(-not $checkResult){ - throw "Not all checks passed!" - } - - InDockerTest: - runs-on: ubuntu-latest - container: ubuntu:24.04 - 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: 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://mirror.it.ubc.ca/ubuntu/pool/main/i/icu/libicu72_72.1-3ubuntu3_amd64.deb - dpkg -i libicu72_72.1-3ubuntu3_amd64.deb - wget https://github.com/PowerShell/PowerShell/releases/download/v7.4.3/powershell_7.4.3-1.deb_amd64.deb - dpkg -i powershell_7.4.3-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 - uses: ./ - with: - client-id: ${{ secrets.SP1_CLIENT_ID }} - tenant-id: ${{ secrets.SP1_TENANT_ID }} - subscription-id: ${{ secrets.SP1_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - - name: Run Azure Cli again - run: | - az group list --output none - - - name: Run Azure PowerShell again - uses: azure/powershell@v2 - with: - azPSVersion: "latest" - inlineScript: | - $checkResult = Get-AzResourceGroup diff --git a/.github/workflows/azure-login-pr-check.yml b/.github/workflows/azure-login-pr-check.yml deleted file mode 100644 index 279cf3cd7..000000000 --- a/.github/workflows/azure-login-pr-check.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: pr-check - -on: - pull_request: - branches: - - master - - 'releases/*' -jobs: - az-login-test: - runs-on: windows-latest - steps: - - name: Checkout from PR branch - uses: actions/checkout@v4 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} - - # Using 20.x version as an example - - name: Set Node.js 20.x for GitHub Action - uses: actions/setup-node@v4 - with: - node-version: 20.x - - - name: installing node_modules - run: npm install - - - name: Build GitHub Action - run: npm run build - - - name: Run mock test - run: npm run test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45da7c8a3..2ca62c4cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,12 +18,12 @@ jobs: steps: - name: 'Checking out repo code' - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set Node.js 20.x for GitHub Action - uses: actions/setup-node@v4 + - name: Set Node.js 24.x for GitHub Action + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 20.x + node-version: 24.x - name: 'Validate build' run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ca4c475ec..ee7a94e6b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,18 +19,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 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@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -44,4 +44,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 diff --git a/.github/workflows/defaultLabels.yml b/.github/workflows/defaultLabels.yml index b9c165487..27382ba84 100644 --- a/.github/workflows/defaultLabels.yml +++ b/.github/workflows/defaultLabels.yml @@ -14,7 +14,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/stale@v8 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 name: Setting issue as idle with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -25,7 +25,7 @@ jobs: operations-per-run: 100 exempt-issue-labels: 'backlog' - - uses: actions/stale@v8 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 name: Setting PR as idle with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -35,7 +35,7 @@ jobs: days-before-close: -1 operations-per-run: 100 - - uses: actions/stale@v8 + - uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0 name: Close issue with no feedback for 20 days with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/markdownlint.yml b/.github/workflows/markdownlint.yml index 76c8789ec..91a56caae 100644 --- a/.github/workflows/markdownlint.yml +++ b/.github/workflows/markdownlint.yml @@ -7,11 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Use Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 20.x + node-version: 24.x - name: Run Markdownlint run: | npm i -g markdownlint-cli2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..510efe23d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,145 @@ +name: Release + +# Manually triggered by a maintainer. Reproduces the Azure Login Action +# release runbook: build -> commit built lib to a release branch -> tag -> +# publish GitHub Release -> move the major tag (e.g. v3). +# +# ADMIN-ONLY: GitHub cannot restrict workflow_dispatch to admins at the trigger +# level (any user with write access can dispatch). Admin-only is enforced two ways: +# 1. environment: release -> requires an admin reviewer to approve the run +# (configure required reviewers under Settings > Environments > release). +# This is the real gate: the destructive steps cannot run without approval. +# 2. The "Ensure triggered by an admin" step below fails fast for non-admins. +on: + workflow_dispatch: + inputs: + version: + description: "Release version, e.g. v3.1.0" + required: true + ref: + description: "Branch to release from. Use master for the current major (v3). For a back-major release (e.g. v2), branch a hotfix off the latest release tag (git checkout -b hotfix/v2.3.2 v2.3.1) and pass that hotfix/* branch." + required: false + default: master + +permissions: + contents: write # push branch/tag, force-move major tag, create the Release + +# Serialize releases: never let two release runs push tags/branches at once. +concurrency: + group: release + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + # Admin gate: this environment must have required reviewers (admins) configured + # in repo settings. The run pauses here until an admin approves. + environment: release + steps: + - name: Ensure triggered by an admin + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + PERM=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq '.permission') + echo "Actor '$ACTOR' has repository permission: $PERM" + if [[ "$PERM" != "admin" ]]; then + echo "::error::Release must be triggered by a repository admin (actor has '$PERM')." + exit 1 + fi + + - name: Validate version input + env: + V: ${{ inputs.version }} + run: | + if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::version must look like v3.1.0 (got '$V')" + exit 1 + fi + + - name: Validate release ref + env: + REF: ${{ inputs.ref }} + run: | + # Only allow releasing from vetted lines: master (current major) and + # the v2 maintenance branch. Prevents shipping arbitrary unreviewed + # branches straight to consumers via the moved major tag. + case "$REF" in + master|hotfix/*) echo "ref '$REF' allowed" ;; + *) echo "::error::ref '$REF' not permitted for release (allowed: master, or a hotfix/* branch based on the latest release tag for a back-major release)"; exit 1 ;; + esac + + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.ref }} # master for the current major; a hotfix/* branch for a back-major release + fetch-depth: 0 + fetch-tags: true + + - name: Ensure version is new + env: + V: ${{ inputs.version }} + run: | + if git ls-remote --exit-code --tags origin "refs/tags/$V" >/dev/null 2>&1; then + echo "::error::tag $V already exists"; exit 1 + fi + if git ls-remote --exit-code --heads origin "refs/heads/releases/$V" >/dev/null 2>&1; then + echo "::error::branch releases/$V already exists"; exit 1 + fi + + - name: Determine runtime node from action.yml + id: node + shell: bash + run: | + ver=$(grep "using:" action.yml | grep -oE "node[0-9]+" | grep -oE "[0-9]+" | head -1) + echo "version=$ver" >> "$GITHUB_OUTPUT" + + - uses: actions/setup-node@v6 + with: + node-version: ${{ steps.node.outputs.version }} # matches action.yml runs.using + cache: npm + + - name: Install, build, test + run: | + npm ci + npm run build + npm test + + - name: Create release branch with built lib + env: + V: ${{ inputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git checkout -b "releases/$V" + git add -f lib + git commit -m "prepare release $V" + git push --set-upstream origin "releases/$V" + + - name: Create and push tag + env: + V: ${{ inputs.version }} + run: | + git tag -a -m "$V" "$V" + git push origin "$V" + + - name: Move major tag (e.g. v3) + env: + V: ${{ inputs.version }} + run: | + MAJOR="${V%%.*}" # v3.1.0 -> v3 + git tag -d "$MAJOR" || true + git tag -a "$MAJOR" -m "Update $MAJOR to $V" + git push origin "$MAJOR" -f + + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + V: ${{ inputs.version }} + run: | + # No --latest flag: GitHub automatically marks the highest-version + # release as "Latest", so a back-major release (e.g. a v2 hotfix) does + # not steal the badge from the current major. + gh release create "$V" \ + --title "Azure Login Action $V" \ + --generate-notes diff --git a/.github/workflows/rollback.yml b/.github/workflows/rollback.yml new file mode 100644 index 000000000..74f127e99 --- /dev/null +++ b/.github/workflows/rollback.yml @@ -0,0 +1,109 @@ +name: Rollback + +# Manually triggered by a maintainer to undo a bad release. A release moves the +# major tag (e.g. v3) forward to the new version; consumers pinned to +# `azure/login@v3` immediately get it. If that release is broken, rollback +# re-points the major tag back to a known-good, already-published version tag +# (e.g. v3.0.1), which contains the built lib/. This is the fast consumer-facing +# fix. It does NOT delete the bad tag/branch/release by default - deletion is +# destructive (someone may have pinned the exact version) and is left to a +# deliberate manual step if ever needed. +# +# ADMIN-ONLY: enforced the same two ways as release.yml: +# 1. environment: release -> requires an admin reviewer to approve the run. +# 2. The "Ensure triggered by an admin" step fails fast for non-admins. +on: + workflow_dispatch: + inputs: + target_version: + description: "Known-good version to roll the major tag back to, e.g. v3.0.1 (must be an existing tag)" + required: true + +permissions: + contents: write # force-move the major tag, update the Release + +# Share the 'release' concurrency group so a rollback can never race an +# in-flight release (both push the same major tag). +concurrency: + group: release + cancel-in-progress: false + +jobs: + rollback: + runs-on: ubuntu-latest + # Admin gate: this environment must have required reviewers (admins) + # configured in repo settings. The run pauses here until an admin approves. + environment: release + steps: + - name: Ensure triggered by an admin + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + run: | + PERM=$(gh api "repos/$REPO/collaborators/$ACTOR/permission" --jq '.permission') + echo "Actor '$ACTOR' has repository permission: $PERM" + if [[ "$PERM" != "admin" ]]; then + echo "::error::Rollback must be triggered by a repository admin (actor has '$PERM')." + exit 1 + fi + + - name: Validate target_version input + env: + V: ${{ inputs.target_version }} + run: | + if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::target_version must look like v3.0.1 (got '$V')" + exit 1 + fi + + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Verify target tag exists and contains built lib + env: + V: ${{ inputs.target_version }} + run: | + # The target must be an existing published version tag... + if ! git rev-parse -q --verify "refs/tags/$V" >/dev/null; then + echo "::error::tag $V does not exist - can only roll back to an already-published version" + exit 1 + fi + # ...and it must carry the built entry points (main + post), or + # consumers would break worse than before (the major tag must always + # point at runnable code). action.yml uses lib/main/index.js and + # lib/cleanup/index.js. + git checkout --quiet "$V" + missing="" + [[ -f lib/main/index.js ]] || missing="$missing lib/main/index.js" + [[ -f lib/cleanup/index.js ]] || missing="$missing lib/cleanup/index.js" + if [[ -n "$missing" ]]; then + echo "::error::tag $V is missing built entry point(s):$missing - refusing to point the major tag at unbuilt code" + exit 1 + fi + + - name: Re-point major tag to target + env: + V: ${{ inputs.target_version }} + run: | + MAJOR="${V%%.*}" # v3.0.1 -> v3 + CURRENT=$(git rev-parse "refs/tags/$MAJOR^{commit}" 2>/dev/null || echo "none") + TARGET=$(git rev-parse "$V^{commit}") + echo "Rolling major $MAJOR: $CURRENT -> $TARGET ($V)" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -d "$MAJOR" || true + git tag -a "$MAJOR" -m "Rollback $MAJOR to $V" "$V" + git push origin "$MAJOR" -f + + - name: Summary + env: + V: ${{ inputs.target_version }} + run: | + MAJOR="${V%%.*}" + echo "### Rollback complete" >> "$GITHUB_STEP_SUMMARY" + echo "- Major tag \`$MAJOR\` now points at \`$V\`" >> "$GITHUB_STEP_SUMMARY" + echo "- Consumers using \`azure/login@$MAJOR\` now get \`$V\`" >> "$GITHUB_STEP_SUMMARY" + echo "- The bad tag/branch/release were NOT deleted (delete manually if required)." >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 18e337ddd..fef07358f 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,12 @@ typings/ .DS_Store Thumbs.db +# IDE / editor +.vs/ +.vscode/ +.idea/ +*.swp + # Ignore built ts files __tests__/runner/* lib/**/* \ No newline at end of file diff --git a/README.md b/README.md index 76ded9178..486be60c0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ # Azure Login Action - [Azure Login Action](#azure-login-action) + - [Supported Versions](#supported-versions) + - [Version Selection](#version-selection) + - [Major-version tag](#major-version-tag) + - [Exact version tag](#exact-version-tag) + - [Branch reference](#branch-reference) + - [Commit SHA](#commit-sha) + - [Security Updates](#security-updates) - [Input Parameters](#input-parameters) - [`client-id`](#client-id) - [`subscription-id`](#subscription-id) @@ -20,6 +27,8 @@ - [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) + - [Troubleshooting](#troubleshooting) + - [OIDC login fails with `AADSTS700213` / `AADSTS7002138` (no matching federated identity record)](#oidc-login-fails-with-aadsts700213--aadsts7002138-no-matching-federated-identity-record) - [Security hardening](#security-hardening) - [Azure CLI dependency](#azure-cli-dependency) - [Reference](#reference) @@ -48,6 +57,80 @@ Azure Login Action supports different ways of authentication with Azure. > [!WARNING] > Avoid using managed identity login on self-hosted runners in public repositories. Managed identities enable secure authentication with Azure resources and obtain Microsoft Entra ID tokens without the need for explicit credential management. Any user can open pull requests against your repository and access your self-hosted runners without credentials. See more details in [self-hosted runner security](https://docs.github.com/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#self-hosted-runner-security). +** ** + +> [!WARNING] +> Only pass values from `${{ secrets.* }}` into `client-id`, `tenant-id`, `subscription-id`, and `creds`. Do not pipe values from `${{ github.event.* }}` (pull request titles, issue comments, `workflow_dispatch` inputs, branch names, etc.) into these inputs. Untrusted values in these fields can allow attackers to influence the Azure identity the action logs in as. + +** ** + +> [!WARNING] +> Only set `enable-AzPSSession: true` if your workflow runs Azure PowerShell (`Az.*`) cmdlets. If your workflow only uses the Azure CLI (`az ...`), leave `enable-AzPSSession` unset (the default is `false`). Enabling it launches an additional PowerShell login step that is unnecessary for CLI-only workflows. + +## Supported Versions + +Azure Login follows a major-version support model. + +| Version | Status | +| --- | --- | +| v3 | Supported | +| v2 | Maintenance mode (security fixes only) | +| v1 | End of Life (EOL) | + +New features are released only to supported versions. Security fixes are released to supported versions and versions in maintenance mode. + +Customers are strongly encouraged to use the latest v3 release. + +## Version Selection + +GitHub Actions users can reference Azure Login using several forms. + +### Major-version tag + +```yaml +uses: azure/login@v3 +``` + +Receives compatible updates, including security fixes, released to the referenced major version. + +### Exact version tag + +```yaml +uses: azure/login@v2.4.0 +``` + +Remains pinned to that specific release and does not automatically receive future fixes or updates. + +### Branch reference + +```yaml +uses: azure/login@master +``` + +Receives updates from the referenced branch. For stable workflows, use a supported major-version tag or pin to a full-length commit SHA. + +### Commit SHA + +```yaml +uses: azure/login@ +``` + +Remains pinned to that commit and does not automatically receive future fixes or updates. + +## Security Updates + +Security fixes are released to supported versions and versions in maintenance mode. Customers using exact version tags or commit SHA references must explicitly upgrade to a patched release to receive security fixes. + +```yaml +# Automatically receives future v3 security updates +uses: azure/login@v3 + +# Does not automatically receive future updates +uses: azure/login@v2.4.0 +``` + +Customers using v1 should migrate to v3. End-of-life releases no longer receive updates or security fixes. + ## Input Parameters |Parameter Name|Required?|Type|Default Value|Description| @@ -72,6 +155,9 @@ It's better to create a GitHub Action secret for this parameter when using it. R Refer to [Login With OpenID Connect (OIDC)](#login-with-openid-connect-oidc-recommended) and [Login With User-assigned Managed Identity](#login-with-user-assigned-managed-identity) for its usage. +> [!NOTE] +> The action registers the `client-id` value as a secret (via `core.setSecret`) so it is masked in workflow logs. Some enterprises treat the client ID as sensitive, and masking also prevents it from being printed accidentally, which matters in public repositories. `tenant-id` and `subscription-id` are not masked. + ### `subscription-id` The input parameter `subscription-id` specifies the login subscription id. @@ -193,7 +279,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Azure login - uses: azure/login@v2 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -223,7 +309,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Azure login - uses: azure/login@v2 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -238,7 +324,7 @@ jobs: az account show - name: Azure PowerShell script - uses: azure/powershell@v2 + uses: azure/powershell@v3 with: azPSVersion: "latest" inlineScript: | @@ -285,7 +371,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: azure/login@v2 + - uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} @@ -312,7 +398,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: azure/login@v2 + - uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} enable-AzPSSession: true @@ -325,7 +411,7 @@ jobs: az account show - name: Azure PowerShell script - uses: azure/powershell@v2 + uses: azure/powershell@v3 with: azPSVersion: "latest" inlineScript: | @@ -335,7 +421,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@v3 with: creds: '{"clientId":"${{ secrets.AZURE_CLIENT_ID }}","clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZURE_TENANT_ID }}"}' ``` @@ -379,7 +465,7 @@ jobs: runs-on: self-hosted steps: - name: Azure login - uses: azure/login@v2 + uses: azure/login@v3 with: auth-type: IDENTITY tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -396,7 +482,7 @@ jobs: az account show - name: Azure PowerShell script - uses: azure/powershell@v2 + uses: azure/powershell@v3 with: azPSVersion: "latest" inlineScript: | @@ -445,7 +531,7 @@ jobs: runs-on: self-hosted steps: - name: Azure login - uses: azure/login@v2 + uses: azure/login@v3 with: auth-type: IDENTITY client-id: ${{ secrets.AZURE_CLIENT_ID }} @@ -463,7 +549,7 @@ jobs: az account show - name: Azure PowerShell script - uses: azure/powershell@v2 + uses: azure/powershell@v3 with: azPSVersion: "latest" inlineScript: | @@ -485,7 +571,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: azure/login@v2 + - uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} environment: 'AzureUSGovernment' @@ -494,6 +580,9 @@ jobs: ### Login to Azure Stack Hub +> [!NOTE] +> Azure CLI versions newer than 2.66.x no longer support Azure Stack Hub. To use `environment: 'AzureStack'`, pin Azure CLI to 2.66.x (LTS), for example via the [Azure CLI action](https://github.com/Azure/cli) with `azcliversion: 2.66.0`. See the [Azure CLI notice for Azure Stack Hub customers](https://learn.microsoft.com/cli/azure/whats-new-overview?view=azure-cli-latest#important-notice-for-azure-stack-hub-customers). + ```yaml # File: .github/workflows/workflow.yml @@ -507,7 +596,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: azure/login@v2 + - uses: azure/login@v3 with: creds: ${{ secrets.AZURE_CREDENTIALS }} environment: 'AzureStack' @@ -534,7 +623,7 @@ jobs: steps: - name: Azure Login - uses: azure/login@v2 + uses: azure/login@v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -549,7 +638,7 @@ jobs: az account show - name: Run Azure PowerShell - uses: azure/powershell@v2 + uses: azure/powershell@v3 with: azPSVersion: "latest" inlineScript: | @@ -597,7 +686,7 @@ jobs: # enable cleanup for the 1st Azure Login - name: Azure Login - uses: azure/login@v2 + uses: azure/login@v3 env: AZURE_LOGIN_PRE_CLEANUP: true AZURE_LOGIN_POST_CLEANUP: true @@ -611,7 +700,7 @@ jobs: # disable cleanup for all other Azure Login - name: Azure Login 2 - uses: azure/login@v2 + uses: azure/login@v3 env: AZURE_LOGIN_PRE_CLEANUP: false AZURE_LOGIN_POST_CLEANUP: false @@ -625,7 +714,7 @@ jobs: # disable cleanup for all other Azure Login - name: Azure Login 3 - uses: azure/login@v2 + uses: azure/login@v3 env: AZURE_LOGIN_PRE_CLEANUP: false AZURE_LOGIN_POST_CLEANUP: false @@ -652,7 +741,7 @@ jobs: steps: - name: Azure Login - uses: azure/login@v2 + uses: azure/login@v3 env: AZURE_LOGIN_PRE_CLEANUP: ${{ startsWith(runner.name, 'GitHub Actions') }} AZURE_LOGIN_POST_CLEANUP: ${{ startsWith(runner.name, 'GitHub Actions') }} @@ -666,6 +755,29 @@ jobs: ``` +## Troubleshooting + +### OIDC login fails with `AADSTS700213` / `AADSTS7002138` (no matching federated identity record) + +When logging in with OIDC, the login may fail with an error similar to: + +```text +Error: AADSTS700213: No matching federated identity record found for presented assertion subject 'repo:/:environment:production'. +``` + +or: + +```text +Error: AADSTS7002138: No matching federated identity record found for presented assertion subject 'repo:/:ref:refs/heads/main'. The subject matches with case-insensitive comparison, but not with case-sensitive comparison. +``` + +This means Microsoft Entra ID could not find a federated identity credential whose **Subject** exactly matches the subject in the OIDC token that GitHub presented. The token's subject is shown in the run log under `Federated token details` as `subject claim`. Two common causes: + +- **Case mismatch.** Federated credential subjects are matched **case-sensitively**. If your organization, repository, branch, or environment name uses uppercase characters (for example `repo:My-Org/My-Repo`), the federated credential Subject must use the exact same casing as the `subject claim` in the run log. +- **Subject includes GitHub numeric IDs.** When you create the federated credential in the Azure portal and fill in the optional GitHub owner/repository ID fields, the portal generates a Subject of the form `repo:@/@::`. The OIDC token GitHub sends does **not** include those numeric IDs (its subject is `repo:/::`), so it will never match. Create or edit the federated credential **without** the owner/repository IDs so the Subject matches the token exactly. + +In both cases, set the federated credential Subject to exactly match the `subject claim` shown in your run's `Federated token details`. See [Configure a federated identity credential](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp#github-actions) for details. + ## Security hardening > [!WARNING] @@ -679,7 +791,7 @@ Internally in this action, we use azure CLI and execute `az login` with the cred ### GitHub Action -[GitHub Actions](https://help.github.com/articles/about-github-actions) gives you the flexibility to build an automated software development lifecycle workflow. +[GitHub Actions](https://docs.github.com/actions) gives you the flexibility to build an automated software development lifecycle workflow. ### GitHub Actions for deploying to Azure diff --git a/__tests__/LoginConfig.test.ts b/__tests__/LoginConfig.test.ts index a4933903a..ee5d67b2c 100644 --- a/__tests__/LoginConfig.test.ts +++ b/__tests__/LoginConfig.test.ts @@ -245,7 +245,7 @@ describe("LoginConfig Test", () => { let loginConfig = new LoginConfig(); await loginConfig.initialize(); - testValidateWithErrorMessage(loginConfig, "Ensure subscriptionId is supplied."); + testValidateWithErrorMessage(loginConfig, "Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'."); }); test('validate without subscriptionId and allowNoSubscriptionsLogin=true', async () => { diff --git a/__tests__/PowerShell/AzPSScriptBuilder.test.ts b/__tests__/PowerShell/AzPSScriptBuilder.test.ts index 862d7a0f5..a1dba885c 100644 --- a/__tests__/PowerShell/AzPSScriptBuilder.test.ts +++ b/__tests__/PowerShell/AzPSScriptBuilder.test.ts @@ -1,7 +1,7 @@ -import AzPSSCriptBuilder from "../../src/PowerShell/AzPSScriptBuilder"; +import AzPSScriptBuilder from "../../src/PowerShell/AzPSScriptBuilder"; import { LoginConfig } from "../../src/common/LoginConfig"; -describe("Getting AzLogin PS script", () => { +describe("Building the Az PS login invocation", () => { function setEnv(name: string, value: string) { process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value; @@ -10,7 +10,7 @@ describe("Getting AzLogin PS script", () => { function cleanEnv() { for (const envKey in process.env) { if (envKey.startsWith('INPUT_')) { - delete process.env[envKey] + delete process.env[envKey]; } } } @@ -19,135 +19,246 @@ describe("Getting AzLogin PS script", () => { cleanEnv(); }); - test('getImportLatestModuleScript', () => { - expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("(Get-Module -Name 'TestModule' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path"); - expect(AzPSSCriptBuilder.getImportLatestModuleScript("TestModule")).toContain("Import-Module -Name $latestModulePath"); + test('getImportLatestModuleScript still emits the interpolated module-import script', () => { + expect(AzPSScriptBuilder.getImportLatestModuleScript("TestModule")).toContain("(Get-Module -Name 'TestModule' -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Path"); + expect(AzPSScriptBuilder.getImportLatestModuleScript("TestModule")).toContain("Import-Module -Name $latestModulePath"); }); - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true', () => { + test('getScriptPath resolves to AzPSLogin.ps1 next to the compiled module', () => { + expect(AzPSScriptBuilder.getScriptPath()).toMatch(/AzPSLogin\.ps1$/); + }); + + test('SP + secret: values ride as pwsh params; secret rides via env var', () => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); setEnv('allow-no-subscriptions', 'true'); setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { + const creds = { 'clientId': 'client-id', - 'clientSecret': "client-secret", + 'clientSecret': 'client-secret', 'tenantId': 'tenant-id', 'subscriptionId': 'subscription-id' - } + }; setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('service principal with secret'); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ methodName, args, env }) => { + expect(methodName).toBe('service principal with secret'); + expect(args[0]).toBe('-File'); + expect(args[1]).toMatch(/AzPSLogin\.ps1$/); + expect(args).toEqual(expect.arrayContaining([ + '-Environment', 'azurecloud', + '-AuthType', 'SERVICE_PRINCIPAL', + '-Tenant', 'tenant-id', + '-Subscription', 'subscription-id', + '-ApplicationId', 'client-id', + ])); + expect(env[AzPSScriptBuilder.ENV_SP_SECRET]).toBe('client-secret'); + expect(env[AzPSScriptBuilder.ENV_FEDERATED_TOKEN]).toBeUndefined(); }); }); - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=true, secret with single-quote', () => { + test('SP + OIDC: federated token rides via env var; no client secret', () => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); + setEnv('client-id', 'client-id'); setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { - 'clientId': 'client-id', - 'clientSecret': "client-se'cret", - 'tenantId': 'tenant-id', - 'subscriptionId': 'subscription-id' - } - setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('service principal with secret'); + jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => { loginConfig.federatedToken = "fake-token"; }); + + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ methodName, args, env }) => { + expect(methodName).toBe('OIDC'); + expect(args).toEqual(expect.arrayContaining([ + '-Tenant', 'tenant-id', + '-Subscription', 'subscription-id', + '-ApplicationId', 'client-id', + ])); + expect(env[AzPSScriptBuilder.ENV_FEDERATED_TOKEN]).toBe('fake-token'); + expect(env[AzPSScriptBuilder.ENV_SP_SECRET]).toBeUndefined(); }); }); - test('getAzPSLoginScript for SP+secret with allowNoSubscriptionsLogin=false', () => { + test('system-assigned MI: no ApplicationId param, no env vars', () => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); // same as true + setEnv('allow-no-subscriptions', 'false'); + setEnv('subscription-id', 'subscription-id'); + setEnv('auth-type', 'IDENTITY'); + + const loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ methodName, args, env }) => { + expect(methodName).toBe('system-assigned managed identity'); + expect(args).toEqual(expect.arrayContaining([ + '-AuthType', 'IDENTITY', + '-Subscription', 'subscription-id', + ])); + expect(args).not.toContain('-ApplicationId'); + expect(Object.keys(env)).toHaveLength(0); + }); + }); + + test('system-assigned MI without subscription id: subscription param omitted', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('auth-type', 'IDENTITY'); + + const loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ methodName, args }) => { + expect(methodName).toBe('system-assigned managed identity'); + expect(args).not.toContain('-Subscription'); + }); + }); + + test('user-assigned MI: ApplicationId param present, no env vars', () => { + setEnv('environment', 'azurecloud'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'IDENTITY'); + setEnv('client-id', 'client-id'); + + const loginConfig = new LoginConfig(); + loginConfig.initialize(); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ methodName, args, env }) => { + expect(methodName).toBe('user-assigned managed identity'); + expect(args).toEqual(expect.arrayContaining([ + '-AuthType', 'IDENTITY', + '-ApplicationId', 'client-id', + ])); + expect(Object.keys(env)).toHaveLength(0); + }); + }); + + test('AzureStack: ArmEndpoint passed as param', () => { + setEnv('environment', 'azurestack'); + setEnv('enable-AzPSSession', 'true'); + setEnv('allow-no-subscriptions', 'true'); setEnv('auth-type', 'SERVICE_PRINCIPAL'); - let creds = { + const creds = { 'clientId': 'client-id', 'clientSecret': 'client-secret', 'tenantId': 'tenant-id', - 'subscriptionId': 'subscription-id' - } + 'subscriptionId': 'subscription-id', + 'resourceManagerEndpointUrl': 'https://management.azurestack.local/' + }; setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('service principal with secret'); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args }) => { + expect(args).toEqual(expect.arrayContaining([ + '-ArmEndpoint', 'https://management.azurestack.local/', + ])); }); }); - test('getAzPSLoginScript for OIDC', () => { - setEnv('environment', 'azurecloud'); + test('SECURITY: adversarial ArmEndpoint travels as a discrete argv element', () => { + setEnv('environment', 'azurestack'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - setEnv('tenant-id', 'tenant-id'); - setEnv('subscription-id', 'subscription-id'); - setEnv('client-id', 'client-id'); + setEnv('allow-no-subscriptions', 'true'); setEnv('auth-type', 'SERVICE_PRINCIPAL'); + const nasty = "https://mgmt.local/' ; Start-Process calc ; $x='"; + const creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id', + 'resourceManagerEndpointUrl': nasty + }; + setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const loginConfig = new LoginConfig(); loginConfig.initialize(); - jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => {loginConfig.federatedToken = "fake-token";}); - return AzPSSCriptBuilder.getAzPSLoginScript(loginConfig).then(([loginMethod, loginScript]) => { - expect(loginScript.includes("Connect-AzAccount -ServicePrincipal -Environment 'azurecloud' -Tenant 'tenant-id' -Subscription 'subscription-id' -ApplicationId 'client-id' -FederatedToken 'fake-token' -InformationAction Ignore | out-null;")).toBeTruthy(); - expect(loginMethod).toBe('OIDC'); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args }) => { + const armIndex = args.indexOf('-ArmEndpoint'); + expect(armIndex).toBeGreaterThan(-1); + expect(args[armIndex + 1]).toBe(nasty); + const otherArgs = args.filter((_, i) => i !== armIndex + 1); + expect(otherArgs.some(a => a.includes(nasty))).toBe(false); }); }); - test('getAzPSLoginScript for System MI', () => { + // Structural safety: no matter how nasty a value is, it can never be re-parsed + // as PowerShell code because it's a distinct argv element / env var, not a + // substring inside a script literal. + const NASTY_VALUES = [ + "abc' ; Start-Process calc ; $x='", + 'abc"; whoami ; #', + "abc\nStart-Process calc", + ]; + + test.each(NASTY_VALUES)('SECURITY: adversarial tenant value %j travels as a discrete argv element', (nasty) => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - setEnv('subscription-id', 'subscription-id'); - setEnv('auth-type', 'IDENTITY'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + const creds = { + 'clientId': 'client-id', + 'clientSecret': 'client-secret', + 'tenantId': nasty, + 'subscriptionId': 'subscription-id' + }; + setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('system-assigned managed identity'); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args }) => { + const tenantIndex = args.indexOf('-Tenant'); + expect(tenantIndex).toBeGreaterThan(-1); + expect(args[tenantIndex + 1]).toBe(nasty); + const otherArgs = args.filter((_, i) => i !== tenantIndex + 1); + expect(otherArgs.some(a => a.includes(nasty))).toBe(false); }); }); - test('getAzPSLoginScript for System MI without subscription id', () => { + test('SECURITY: adversarial client-secret rides in env var only, never in argv', () => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'false'); - // setEnv('subscription-id', 'subscription-id'); - setEnv('auth-type', 'IDENTITY'); + setEnv('allow-no-subscriptions', 'true'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + const nasty = "abc' ; Start-Process calc ; $x='"; + const creds = { + 'clientId': 'client-id', + 'clientSecret': nasty, + 'tenantId': 'tenant-id', + 'subscriptionId': 'subscription-id' + }; + setEnv('creds', JSON.stringify(creds)); - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('system-assigned managed identity'); + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args, env }) => { + expect(env[AzPSScriptBuilder.ENV_SP_SECRET]).toBe(nasty); + expect(args.some(a => a.includes(nasty))).toBe(false); }); }); - test('getAzPSLoginScript for user-assigned MI', () => { + test('SECURITY: federated token rides in env var only, never in argv', () => { setEnv('environment', 'azurecloud'); setEnv('enable-AzPSSession', 'true'); - setEnv('allow-no-subscriptions', 'true'); - setEnv('auth-type', 'IDENTITY'); + setEnv('allow-no-subscriptions', 'false'); + setEnv('tenant-id', 'tenant-id'); + setEnv('subscription-id', 'subscription-id'); setEnv('client-id', 'client-id'); + setEnv('auth-type', 'SERVICE_PRINCIPAL'); + const nasty = "abc' ; Start-Process calc ; $x='"; - let loginConfig = new LoginConfig(); + const 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(loginMethod).toBe('user-assigned managed identity'); + jest.spyOn(loginConfig, 'getFederatedToken').mockImplementation(async () => { loginConfig.federatedToken = nasty; }); + + return AzPSScriptBuilder.getAzPSLoginInvocation(loginConfig).then(({ args, env }) => { + expect(env[AzPSScriptBuilder.ENV_FEDERATED_TOKEN]).toBe(nasty); + expect(args.some(a => a.includes(nasty))).toBe(false); }); }); -}); \ No newline at end of file +}); diff --git a/action.yml b/action.yml index 44c1f66a6..bf52a453b 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ branding: icon: 'login.svg' color: 'blue' runs: - using: 'node20' + using: 'node24' main: 'lib/main/index.js' post-if: (!env.AZURE_LOGIN_POST_CLEANUP || env.AZURE_LOGIN_POST_CLEANUP != 'false') post: 'lib/cleanup/index.js' diff --git a/package-lock.json b/package-lock.json index 47ff5fc3a..1d1af4a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,20 @@ { "name": "login", - "version": "2.2.0", + "version": "3.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "login", - "version": "2.2.0", "license": "MIT", "dependencies": { - "@actions/core": "1.9.1", + "@actions/core": "1.11.1", "@actions/exec": "^1.0.1", - "@actions/io": "^1.0.1", - "package-lock": "^1.0.3" + "@actions/io": "^1.0.1" }, "devDependencies": { "@types/jest": "^29.2.4", - "@types/node": "^20.11.1", + "@types/node": "^24.0.0", "@vercel/ncc": "^0.38.1", "jest": "^29.3.1", "jest-circus": "^29.3.1", @@ -25,18 +23,13 @@ } }, "node_modules/@actions/core": { - "version": "1.9.1", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "license": "MIT", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/core/node_modules/uuid": { - "version": "8.3.2", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" } }, "node_modules/@actions/exec": { @@ -77,73 +70,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.20.5", "dev": true, @@ -321,7 +261,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -329,7 +271,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -345,91 +289,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.20.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.5", - "@babel/types": "^7.20.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.23.4", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.29.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.9", - "dev": true, - "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -601,13 +482,15 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -634,13 +517,14 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1077,11 +961,13 @@ } }, "node_modules/@types/node": { - "version": "20.11.19", + "version": "24.12.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", + "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "node_modules/@types/prettier": { @@ -1172,13 +1058,6 @@ "sprintf-js": "~1.0.2" } }, - "node_modules/asyncc": { - "version": "2.0.6", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/babel-jest": { "version": "29.3.1", "dev": true, @@ -1267,10 +1146,14 @@ }, "node_modules/balanced-match": { "version": "1.0.2", + "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1278,11 +1161,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1449,15 +1334,9 @@ "dev": true, "license": "MIT" }, - "node_modules/commander": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/concat-map": { "version": "0.0.1", + "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -1466,7 +1345,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1638,7 +1519,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -1662,8 +1545,24 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "dev": true, @@ -1706,6 +1605,7 @@ }, "node_modules/glob": { "version": "7.2.3", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -1746,14 +1646,6 @@ "node": ">= 0.4.0" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "dev": true, @@ -1795,6 +1687,7 @@ }, "node_modules/inflight": { "version": "1.0.6", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -1803,6 +1696,7 @@ }, "node_modules/inherits": { "version": "2.0.4", + "dev": true, "license": "ISC" }, "node_modules/is-arrayish": { @@ -1839,6 +1733,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -2476,11 +2372,15 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -2550,10 +2450,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -2603,11 +2499,13 @@ "license": "MIT" }, "node_modules/micromatch": { - "version": "4.0.5", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2623,7 +2521,10 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -2673,6 +2574,7 @@ }, "node_modules/once": { "version": "1.4.0", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -2739,20 +2641,6 @@ "node": ">=6" } }, - "node_modules/package-lock": { - "version": "1.0.3", - "license": "Unlicense", - "dependencies": { - "asyncc": "^2.0.4", - "commander": "^5.0.0", - "glob": "^7.1.6", - "lodash": "^4.17.15", - "traverse": "^0.6.6" - }, - "bin": { - "package-lock": "bin/package-lock.js" - } - }, "node_modules/parse-json": { "version": "5.2.0", "dev": true, @@ -2780,6 +2668,7 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2799,12 +2688,16 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3115,16 +3008,10 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3134,13 +3021,6 @@ "node": ">=8.0" } }, - "node_modules/traverse": { - "version": "0.6.7", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ts-jest": { "version": "29.0.3", "dev": true, @@ -3229,7 +3109,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -3316,6 +3198,7 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 1ebd3012a..a678091fe 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,9 @@ { "name": "login", - "version": "2.2.0", "description": "Login Azure wraps the az login, allowing for Azure actions to log into Azure", "main": "lib/main/index.js", "scripts": { - "build:main": "ncc build src/main.ts -o lib/main", + "build:main": "ncc build src/main.ts -o lib/main && node scripts/copy-ps-assets.js", "build:cleanup": "ncc build src/cleanup.ts -o lib/cleanup", "build": "npm run build:main && npm run build:cleanup", "test": "jest" @@ -13,7 +12,7 @@ "license": "MIT", "devDependencies": { "@types/jest": "^29.2.4", - "@types/node": "^20.11.1", + "@types/node": "^24.0.0", "@vercel/ncc": "^0.38.1", "jest": "^29.3.1", "jest-circus": "^29.3.1", @@ -21,9 +20,8 @@ "typescript": "^4.9.4" }, "dependencies": { - "@actions/core": "1.9.1", + "@actions/core": "1.11.1", "@actions/exec": "^1.0.1", - "@actions/io": "^1.0.1", - "package-lock": "^1.0.3" + "@actions/io": "^1.0.1" } } diff --git a/scripts/copy-ps-assets.js b/scripts/copy-ps-assets.js new file mode 100644 index 000000000..801255167 --- /dev/null +++ b/scripts/copy-ps-assets.js @@ -0,0 +1,13 @@ +// Copies the static PowerShell login script into the compiled action bundle. +// Run as part of `npm run build:main` so that lib/main/index.js can locate +// AzPSLogin.ps1 via `path.join(__dirname, 'AzPSLogin.ps1')` at runtime. + +const fs = require('fs'); +const path = require('path'); + +const src = path.join(__dirname, '..', 'src', 'PowerShell', 'AzPSLogin.ps1'); +const dest = path.join(__dirname, '..', 'lib', 'main', 'AzPSLogin.ps1'); + +fs.mkdirSync(path.dirname(dest), { recursive: true }); +fs.copyFileSync(src, dest); +console.log(`Copied ${path.relative(process.cwd(), src)} -> ${path.relative(process.cwd(), dest)}`); diff --git a/src/Cli/AzureCliLogin.ts b/src/Cli/AzureCliLogin.ts index 2c4c5f8f0..aa3a95ece 100644 --- a/src/Cli/AzureCliLogin.ts +++ b/src/Cli/AzureCliLogin.ts @@ -8,6 +8,7 @@ export class AzureCliLogin { loginConfig: LoginConfig; azPath: string; loginOptions: ExecOptions; + azVersion: string; constructor(loginConfig: LoginConfig) { this.loginConfig = loginConfig; @@ -30,7 +31,12 @@ export class AzureCliLogin { 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(); await this.executeAzCliCommand(["cloud", "set", "-n", this.loginConfig.environment], false); @@ -108,7 +114,20 @@ export class AzureCliLogin { } async loginWithUserAssignedIdentity(args: string[]) { - args.push("--username", this.loginConfig.servicePrincipalId); + 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); + } await this.callCliLogin(args, 'user-assigned managed identity'); } diff --git a/src/PowerShell/AzPSLogin.ps1 b/src/PowerShell/AzPSLogin.ps1 new file mode 100644 index 000000000..c084a62e8 --- /dev/null +++ b/src/PowerShell/AzPSLogin.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)] + [ValidateSet('azurecloud', 'azurechinacloud', 'azureusgovernment', 'azuregermancloud', 'azurestack')] + [string]$Environment, + + [Parameter(Mandatory)] + [ValidateSet('SERVICE_PRINCIPAL', 'IDENTITY')] + [string]$AuthType, + + [string]$Tenant, + + [string]$Subscription, + + [string]$ApplicationId, + + [string]$ArmEndpoint +) + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +try { + if ($Environment -eq 'azurestack') { + if ([string]::IsNullOrEmpty($ArmEndpoint)) { + throw "ArmEndpoint is required when Environment is 'azurestack'." + } + Add-AzEnvironment -Name $Environment -ARMEndpoint $ArmEndpoint | Out-Null + } + + $connectArgs = @{ + Environment = $Environment + InformationAction = 'Ignore' + } + if ($Tenant) { $connectArgs['Tenant'] = $Tenant } + if ($Subscription) { $connectArgs['Subscription'] = $Subscription } + + if ($AuthType -eq 'SERVICE_PRINCIPAL') { + $connectArgs['ServicePrincipal'] = $true + + if ($env:AZURE_LOGIN_ACTION__SP_SECRET) { + $secure = ConvertTo-SecureString $env:AZURE_LOGIN_ACTION__SP_SECRET -AsPlainText -Force + $connectArgs['Credential'] = New-Object System.Management.Automation.PSCredential($ApplicationId, $secure) + Remove-Item Env:AZURE_LOGIN_ACTION__SP_SECRET -ErrorAction SilentlyContinue + } + elseif ($env:AZURE_LOGIN_ACTION__FEDERATED_TOKEN) { + $connectArgs['ApplicationId'] = $ApplicationId + $connectArgs['FederatedToken'] = $env:AZURE_LOGIN_ACTION__FEDERATED_TOKEN + Remove-Item Env:AZURE_LOGIN_ACTION__FEDERATED_TOKEN -ErrorAction SilentlyContinue + } + else { + throw "SERVICE_PRINCIPAL auth requires either AZURE_LOGIN_ACTION__SP_SECRET or AZURE_LOGIN_ACTION__FEDERATED_TOKEN in the environment." + } + } + else { + $connectArgs['Identity'] = $true + if ($ApplicationId) { + $connectArgs['AccountId'] = $ApplicationId + } + } + + Connect-AzAccount @connectArgs | Out-Null + + $output = @{ Success = $true; Result = '' } +} +catch { + $output = @{ Success = $false; Error = $_.Exception.Message } +} +finally { + Remove-Item Env:AZURE_LOGIN_ACTION__SP_SECRET -ErrorAction SilentlyContinue + Remove-Item Env:AZURE_LOGIN_ACTION__FEDERATED_TOKEN -ErrorAction SilentlyContinue +} + +ConvertTo-Json $output diff --git a/src/PowerShell/AzPSLogin.ts b/src/PowerShell/AzPSLogin.ts index 6e0c7bd6a..24103c239 100644 --- a/src/PowerShell/AzPSLogin.ts +++ b/src/PowerShell/AzPSLogin.ts @@ -15,10 +15,10 @@ export class AzPSLogin { core.info(`Running Azure PowerShell Login.`); AzPSUtils.setPSModulePathForGitHubRunner(); await AzPSUtils.importLatestAzAccounts(); - const [loginMethod, loginScript] = await AzPSScriptBuilder.getAzPSLoginScript(this.loginConfig); - core.info(`Attempting Azure PowerShell login by using ${loginMethod}...`); - core.debug(`Azure PowerShell Login Script: ${loginScript}`); - await AzPSUtils.runPSScript(loginScript); + const { methodName, args, env } = await AzPSScriptBuilder.getAzPSLoginInvocation(this.loginConfig); + core.info(`Attempting Azure PowerShell login by using ${methodName}...`); + core.debug(`Azure PowerShell login invocation: pwsh ${JSON.stringify(args)}`); + await AzPSUtils.runPSFile(args, env); console.log(`Running Azure PowerShell Login successfully.`); } } diff --git a/src/PowerShell/AzPSScriptBuilder.ts b/src/PowerShell/AzPSScriptBuilder.ts index 5cd34580f..d5470ea30 100644 --- a/src/PowerShell/AzPSScriptBuilder.ts +++ b/src/PowerShell/AzPSScriptBuilder.ts @@ -1,7 +1,21 @@ +import * as path from 'path'; import { LoginConfig } from '../common/LoginConfig'; +export interface AzPSLoginInvocation { + methodName: string; + args: string[]; + env: Record; +} + export default class AzPSScriptBuilder { + static readonly ENV_SP_SECRET = 'AZURE_LOGIN_ACTION__SP_SECRET'; + static readonly ENV_FEDERATED_TOKEN = 'AZURE_LOGIN_ACTION__FEDERATED_TOKEN'; + + static getScriptPath(): string { + return path.join(__dirname, 'AzPSLogin.ps1'); + } + static getImportLatestModuleScript(moduleName: string): string { let script = `try { $ErrorActionPreference = "Stop" @@ -21,91 +35,45 @@ export default class AzPSScriptBuilder { return script; } - static async getAzPSLoginScript(loginConfig: LoginConfig) { - let loginMethodName = ""; - let commands = ""; + static async getAzPSLoginInvocation(loginConfig: LoginConfig): Promise { + const args: string[] = [ + '-File', AzPSScriptBuilder.getScriptPath(), + '-Environment', loginConfig.environment, + '-AuthType', loginConfig.authType, + ]; + const env: Record = {}; + let methodName: string; - if (loginConfig.environment.toLowerCase() == "azurestack") { - commands += `Add-AzEnvironment -Name '${loginConfig.environment}' -ARMEndpoint '${loginConfig.resourceManagerEndpointUrl}' | out-null;`; + if (loginConfig.tenantId) { + args.push('-Tenant', loginConfig.tenantId); } + if (loginConfig.subscriptionId) { + args.push('-Subscription', loginConfig.subscriptionId); + } + if (loginConfig.environment.toLowerCase() === 'azurestack') { + args.push('-ArmEndpoint', loginConfig.resourceManagerEndpointUrl); + } + if (loginConfig.authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL) { + args.push('-ApplicationId', loginConfig.servicePrincipalId); if (loginConfig.servicePrincipalSecret) { - commands += AzPSScriptBuilder.loginWithSecret(loginConfig); - loginMethodName = 'service principal with secret'; + env[AzPSScriptBuilder.ENV_SP_SECRET] = loginConfig.servicePrincipalSecret; + methodName = 'service principal with secret'; } else { - commands += await AzPSScriptBuilder.loginWithOIDC(loginConfig); - loginMethodName = "OIDC"; + await loginConfig.getFederatedToken(); + env[AzPSScriptBuilder.ENV_FEDERATED_TOKEN] = loginConfig.federatedToken; + methodName = 'OIDC'; } } else { if (loginConfig.servicePrincipalId) { - commands += AzPSScriptBuilder.loginWithUserAssignedIdentity(loginConfig); - loginMethodName = 'user-assigned managed identity'; + args.push('-ApplicationId', loginConfig.servicePrincipalId); + methodName = 'user-assigned managed identity'; } else { - commands += AzPSScriptBuilder.loginWithSystemAssignedIdentity(loginConfig); - loginMethodName = 'system-assigned managed identity'; + methodName = 'system-assigned managed identity'; } } - let script = `try { - $ErrorActionPreference = "Stop" - $WarningPreference = "SilentlyContinue" - $output = @{} - ${commands} - $output['Success'] = $true - $output['Result'] = "" - } - catch { - $output['Success'] = $false - $output['Error'] = $_.exception.Message - } - return ConvertTo-Json $output`; - - return [loginMethodName, script]; - } - - private static loginWithSecret(loginConfig: LoginConfig): string { - let servicePrincipalSecret: string = loginConfig.servicePrincipalSecret.split("'").join("''"); - let loginCmdlet = `$psLoginSecrets = ConvertTo-SecureString '${servicePrincipalSecret}' -AsPlainText -Force; `; - loginCmdlet += `$psLoginCredential = New-Object System.Management.Automation.PSCredential('${loginConfig.servicePrincipalId}', $psLoginSecrets); `; - - let cmdletSuffix = "-Credential $psLoginCredential"; - loginCmdlet += AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - - return loginCmdlet; - } - - private static async loginWithOIDC(loginConfig: LoginConfig) { - await loginConfig.getFederatedToken(); - let cmdletSuffix = `-ApplicationId '${loginConfig.servicePrincipalId}' -FederatedToken '${loginConfig.federatedToken}'`; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - private static loginWithSystemAssignedIdentity(loginConfig: LoginConfig): string { - let cmdletSuffix = ""; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - static loginWithUserAssignedIdentity(loginConfig: LoginConfig): string { - let cmdletSuffix = `-AccountId '${loginConfig.servicePrincipalId}'`; - return AzPSScriptBuilder.psLoginCmdlet(loginConfig.authType, loginConfig.environment, loginConfig.tenantId, loginConfig.subscriptionId, cmdletSuffix); - } - - private static psLoginCmdlet(authType:string, environment:string, tenantId:string, subscriptionId:string, cmdletSuffix:string){ - let loginCmdlet = `Connect-AzAccount `; - if(authType === LoginConfig.AUTH_TYPE_SERVICE_PRINCIPAL){ - loginCmdlet += "-ServicePrincipal "; - }else{ - loginCmdlet += "-Identity "; - } - loginCmdlet += `-Environment '${environment}' `; - if(tenantId){ - loginCmdlet += `-Tenant '${tenantId}' `; - } - if(subscriptionId){ - loginCmdlet += `-Subscription '${subscriptionId}' `; - } - loginCmdlet += `${cmdletSuffix} -InformationAction Ignore | out-null;`; - return loginCmdlet; + return { methodName, args, env }; } } diff --git a/src/PowerShell/AzPSUtils.ts b/src/PowerShell/AzPSUtils.ts index 4b287cfd7..bc5298b6f 100644 --- a/src/PowerShell/AzPSUtils.ts +++ b/src/PowerShell/AzPSUtils.ts @@ -52,6 +52,14 @@ export class AzPSUtils { } static async runPSScript(psScript: string): Promise { + return AzPSUtils.runPwsh(['-Command', psScript]); + } + + static async runPSFile(args: string[], extraEnv: Record = {}): Promise { + return AzPSUtils.runPwsh(args, extraEnv); + } + + private static async runPwsh(args: string[], extraEnv: Record = {}): Promise { let outputString: string = ""; let commandStdErr = false; const options: any = { @@ -69,9 +77,12 @@ export class AzPSUtils { } } }; + if (Object.keys(extraEnv).length > 0) { + options.env = { ...process.env, ...extraEnv }; + } let psPath: string = await io.which(AzPSConstants.PowerShell_CmdName, true); - await exec.exec(`"${psPath}"`, ["-Command", psScript], options) + await exec.exec(`"${psPath}"`, args, options) if (commandStdErr) { throw new Error('Azure PowerShell login failed with errors.'); } diff --git a/src/common/LoginConfig.ts b/src/common/LoginConfig.ts index d3a3e1c74..b9939c588 100644 --- a/src/common/LoginConfig.ts +++ b/src/common/LoginConfig.ts @@ -79,11 +79,16 @@ export class LoginConfig { this.mask(this.federatedToken); } catch (error) { - core.error(`Please make sure to give write permissions to id-token in the workflow.`); + core.error("Failed to fetch federated token from GitHub. Please make sure to give write permissions to id-token in the workflow."); throw error; } - let [issuer, subjectClaim] = await jwtParser(this.federatedToken); - core.info("Federated token details:\n issuer - " + issuer + "\n subject claim - " + subjectClaim); + 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}`); + } } validate() { @@ -99,7 +104,7 @@ export class LoginConfig { } } if (!this.subscriptionId && !this.allowNoSubscriptionsLogin) { - throw new Error("Ensure subscriptionId is supplied."); + throw new Error("Ensure 'subscription-id' is supplied or 'allow-no-subscriptions' is 'true'."); } } @@ -114,5 +119,20 @@ 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']]; -} + 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]]; +} diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 8e9186d32..ecd45df94 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -7,8 +7,9 @@ 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}`; + let actionRef = process.env.GITHUB_ACTION_REF || 'unknown'; + process.env.AZURE_HTTP_USER_AGENT = (!!process.env.AZURE_HTTP_USER_AGENT ? `${process.env.AZURE_HTTP_USER_AGENT} ` : '') + `GITHUBACTIONS/${actionName}@${actionRef}_${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}@${actionRef}_${usrAgentRepo}_${process.env.RUNNER_ENVIRONMENT}_${process.env.GITHUB_RUN_ID}`; } export async function cleanupAzCLIAccounts(): Promise {