diff --git a/Actions/CheckAuthContext/CheckAuthContext.ps1 b/Actions/CheckAuthContext/CheckAuthContext.ps1 new file mode 100644 index 000000000..b83a9f7d0 --- /dev/null +++ b/Actions/CheckAuthContext/CheckAuthContext.ps1 @@ -0,0 +1,48 @@ +Param( + [Parameter(HelpMessage = "Name of the secret to check (e.g., 'adminCenterApiCredentials' or comma-separated list to check multiple)", Mandatory = $true)] + [string] $secretName, + [Parameter(HelpMessage = "Type of authentication (e.g., 'AuthContext' or 'Admin Center Api Credentials')", Mandatory = $false)] + [string] $authType = 'AuthContext', + [Parameter(HelpMessage = "Environment name (for error messages)", Mandatory = $false)] + [string] $environmentName = '' +) + +. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) + +$secrets = $env:Secrets | ConvertFrom-Json | ConvertTo-HashTable + +# Check each secret name in order +$authContext = $null +$foundSecretName = '' +foreach ($name in ($secretName -split ',')) { + $name = $name.Trim() + if ($secrets.ContainsKey($name) -and $secrets."$name") { + Write-Host "Using $name secret" + $authContext = $secrets."$name" + $foundSecretName = $name + break + } +} + +if ($authContext) { + Write-Host "$authType provided in secret $foundSecretName! Using this information for authentication." +} else { + Write-Host "No $authType provided, initiating Device Code flow" + DownloadAndImportBcContainerHelper + $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) + if ($null -eq $authContext) { + OutputError "Failed to acquire authentication context via device code flow." + return + } + + # Build appropriate error message + if ($environmentName) { + $message = "AL-Go needs access to the Business Central Environment $($environmentName.Split(' ')[0]) and could not locate a secret called $($secretName -replace ',', ' or ')" + } + else { + $message = "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($secretName -replace ',', ' or ') (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)" + } + + Add-Content -Encoding UTF8 -Path $ENV:GITHUB_STEP_SUMMARY -Value "$message`n`n$($authContext.message)" + Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" +} diff --git a/Actions/CheckAuthContext/README.md b/Actions/CheckAuthContext/README.md new file mode 100644 index 000000000..255095be5 --- /dev/null +++ b/Actions/CheckAuthContext/README.md @@ -0,0 +1,33 @@ +# Check Auth Context + +Check if Admin Center Api Credentials / AuthContext are provided in secrets. If not, initiate device code flow for authentication. + +## INPUT + +### ENV variables + +| Name | Description | +| :-- | :-- | +| Settings | env.Settings must be set by a prior call to the ReadSettings Action | +| Secrets | env.Secrets must be set to the secrets output from ReadSecrets Action | + +### Parameters + +| Name | Required | Description | Default value | +| :-- | :-: | :-- | :-- | +| shell | | The shell (powershell or pwsh) in which the PowerShell script in this action should run | powershell | +| secretName | Yes | Name of the secret to check (comma-separated list to check multiple in order) | | +| authType | | Type of authentication (e.g., 'AuthContext' or 'Admin Center Api Credentials') | AuthContext | +| environmentName | | Environment name (for error messages when deploying to environments) | | + +## OUTPUT + +### ENV variables + +none + +### OUTPUT variables + +| Name | Description | +| :-- | :-- | +| deviceCode | Device code for authentication (only set if device login is required) | diff --git a/Actions/CheckAuthContext/action.yaml b/Actions/CheckAuthContext/action.yaml new file mode 100644 index 000000000..3014717b3 --- /dev/null +++ b/Actions/CheckAuthContext/action.yaml @@ -0,0 +1,39 @@ +name: Check Auth Context +author: Microsoft Corporation +inputs: + shell: + description: Shell in which you want to run the action (powershell or pwsh) + required: false + default: powershell + secretName: + description: Name of the secret to check (e.g., 'adminCenterApiCredentials' or comma-separated list to check multiple) + required: true + authType: + description: Type of authentication (e.g., 'AuthContext' or 'Admin Center Api Credentials') + required: false + default: 'AuthContext' + environmentName: + description: Environment name (for error messages) + required: false + default: '' +outputs: + deviceCode: + description: Device code for authentication (if device login is required) + value: ${{ steps.CheckAuthContext.outputs.deviceCode }} +runs: + using: composite + steps: + - name: run + shell: ${{ inputs.shell }} + id: CheckAuthContext + env: + _secretName: ${{ inputs.secretName }} + _authType: ${{ inputs.authType }} + _environmentName: ${{ inputs.environmentName }} + run: | + ${{ github.action_path }}/../Invoke-AlGoAction.ps1 -ActionName "CheckAuthContext" -Action { + ${{ github.action_path }}/CheckAuthContext.ps1 -secretName $ENV:_secretName -authType $ENV:_authType -environmentName $ENV:_environmentName + } +branding: + icon: terminal + color: blue diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e730ac4c1..358c2c45b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,6 @@ ### Issues +- Issue 2113 Using the action "Create Online Dev. Environment" fails in Initialization phase - Issue 2107 Publish a specific build mode to an environment - Issue 1915 CICD fails on releases/26.x branch - '26.x' cannot be recognized as a semantic version string diff --git a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml index 174741887..054b48e27 100644 --- a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml +++ b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml @@ -79,24 +79,13 @@ jobs: - name: Check AdminCenterApiCredentials / Initiate Device Login (open to see code) id: authenticate - run: | - $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 - $settings = $env:Settings | ConvertFrom-Json - if ('${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}') { - Write-Host "AdminCenterApiCredentials provided in secret $($settings.adminCenterApiCredentialsSecretName)!" - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication." - } - else { - Write-Host "AdminCenterApiCredentials not provided, initiating Device Code flow" - $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1', $ALGoHelperPath) - . $ALGoHelperPath - DownloadAndImportBcContainerHelper - $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)" - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" - } + uses: microsoft/AL-Go-Actions/CheckAuthContext@main + env: + Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + with: + shell: powershell + secretName: 'adminCenterApiCredentials' + authType: 'Admin Center Api Credentials' CreateDevelopmentEnvironment: needs: [ Initialization ] diff --git a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml index e60df4fc8..df25bdbd3 100644 --- a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml @@ -92,35 +92,13 @@ jobs: - name: Authenticate id: Authenticate if: steps.DetermineDeploymentEnvironments.outputs.UnknownEnvironment == 1 - run: | - $envName = '${{ steps.envName.outputs.envName }}' - $secretName = '' - $secrets = '${{ steps.ReadSecrets.outputs.Secrets }}' | ConvertFrom-Json - $authContext = $null - "$($envName)-AuthContext", "$($envName)_AuthContext", "AuthContext" | ForEach-Object { - if (!($authContext)) { - if ($secrets."$_") { - Write-Host "Using $_ secret as AuthContext" - $authContext = $secrets."$_" - $secretName = $_ - } - } - } - if ($authContext) { - Write-Host "AuthContext provided in secret $secretName!" - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication." - } - else { - Write-Host "No AuthContext provided for $envName, initiating Device Code flow" - $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1', $ALGoHelperPath) - . $ALGoHelperPath - DownloadAndImportBcContainerHelper - $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)" - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" - } + uses: microsoft/AL-Go-Actions/CheckAuthContext@main + env: + Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + with: + shell: powershell + secretName: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' + environmentName: ${{ steps.envName.outputs.envName }} Deploy: needs: [ Initialization ] diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml index 174741887..054b48e27 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml @@ -79,24 +79,13 @@ jobs: - name: Check AdminCenterApiCredentials / Initiate Device Login (open to see code) id: authenticate - run: | - $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 - $settings = $env:Settings | ConvertFrom-Json - if ('${{ fromJson(steps.ReadSecrets.outputs.Secrets).adminCenterApiCredentials }}') { - Write-Host "AdminCenterApiCredentials provided in secret $($settings.adminCenterApiCredentialsSecretName)!" - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "Admin Center Api Credentials was provided in a secret called $($settings.adminCenterApiCredentialsSecretName). Using this information for authentication." - } - else { - Write-Host "AdminCenterApiCredentials not provided, initiating Device Code flow" - $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1', $ALGoHelperPath) - . $ALGoHelperPath - DownloadAndImportBcContainerHelper - $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Admin Center Api and could not locate a secret called $($settings.adminCenterApiCredentialsSecretName) (https://aka.ms/ALGoSettings#AdminCenterApiCredentialsSecretName)`n`n$($authContext.message)" - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" - } + uses: microsoft/AL-Go-Actions/CheckAuthContext@main + env: + Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + with: + shell: powershell + secretName: 'adminCenterApiCredentials' + authType: 'Admin Center Api Credentials' CreateDevelopmentEnvironment: needs: [ Initialization ] diff --git a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml index e60df4fc8..df25bdbd3 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml @@ -92,35 +92,13 @@ jobs: - name: Authenticate id: Authenticate if: steps.DetermineDeploymentEnvironments.outputs.UnknownEnvironment == 1 - run: | - $envName = '${{ steps.envName.outputs.envName }}' - $secretName = '' - $secrets = '${{ steps.ReadSecrets.outputs.Secrets }}' | ConvertFrom-Json - $authContext = $null - "$($envName)-AuthContext", "$($envName)_AuthContext", "AuthContext" | ForEach-Object { - if (!($authContext)) { - if ($secrets."$_") { - Write-Host "Using $_ secret as AuthContext" - $authContext = $secrets."$_" - $secretName = $_ - } - } - } - if ($authContext) { - Write-Host "AuthContext provided in secret $secretName!" - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AuthContext was provided in a secret called $secretName. Using this information for authentication." - } - else { - Write-Host "No AuthContext provided for $envName, initiating Device Code flow" - $ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" - $webClient = New-Object System.Net.WebClient - $webClient.DownloadFile('https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1', $ALGoHelperPath) - . $ALGoHelperPath - DownloadAndImportBcContainerHelper - $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromSeconds(0)) - Add-Content -Encoding UTF8 -path $ENV:GITHUB_STEP_SUMMARY -value "AL-Go needs access to the Business Central Environment $('${{ steps.envName.outputs.envName }}'.Split(' ')[0]) and could not locate a secret called ${{ steps.envName.outputs.envName }}_AuthContext`n`n$($authContext.message)" - Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "deviceCode=$($authContext.deviceCode)" - } + uses: microsoft/AL-Go-Actions/CheckAuthContext@main + env: + Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + with: + shell: powershell + secretName: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' + environmentName: ${{ steps.envName.outputs.envName }} Deploy: needs: [ Initialization ] diff --git a/Tests/CheckAuthContext.Action.Test.ps1 b/Tests/CheckAuthContext.Action.Test.ps1 new file mode 100644 index 000000000..403de8f1e --- /dev/null +++ b/Tests/CheckAuthContext.Action.Test.ps1 @@ -0,0 +1,136 @@ +Get-Module TestActionsHelper | Remove-Module -Force +Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') +$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 + +Describe "CheckAuthContext Action Tests" { + BeforeAll { + $actionName = "CheckAuthContext" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + $scriptName = "$actionName.ps1" + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'scriptPath', Justification = 'False positive.')] + $scriptPath = Join-Path $scriptRoot $scriptName + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName $scriptName + } + + BeforeEach { + $env:GITHUB_STEP_SUMMARY = [System.IO.Path]::GetTempFileName() + $env:GITHUB_OUTPUT = [System.IO.Path]::GetTempFileName() + } + + AfterEach { + Remove-Item $env:GITHUB_STEP_SUMMARY -ErrorAction SilentlyContinue + Remove-Item $env:GITHUB_OUTPUT -ErrorAction SilentlyContinue + } + + It 'Compile Action' { + Invoke-Expression $actionScript + } + + It 'Test action.yaml matches script' { + $outputs = [ordered]@{ + "deviceCode" = "Device code for authentication (if device login is required)" + } + YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -outputs $outputs + } + + It 'Should find first matching secret' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{"adminCenterApiCredentials": "someCredentials"}' + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'adminCenterApiCredentials' + + # Should NOT output deviceCode when secret is found + $output = Get-Content $env:GITHUB_OUTPUT -Raw + $output | Should -Not -Match "deviceCode=" + } + + It 'Should find secret when checking multiple names - first match wins' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{"TestEnv-AuthContext": "firstSecret", "AuthContext": "secondSecret"}' + + Mock Write-Host {} + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'TestEnv-AuthContext,TestEnv_AuthContext,AuthContext' + + # Should NOT output deviceCode when secret is found + $output = Get-Content $env:GITHUB_OUTPUT -Raw + $output | Should -Not -Match "deviceCode=" + + # Should use the first matching secret, not a later one + Should -Invoke Write-Host -ParameterFilter { $Object -eq "Using TestEnv-AuthContext secret" } + } + + It 'Should find fallback secret when primary not found' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{"AuthContext": "fallbackSecret"}' + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'TestEnv-AuthContext,TestEnv_AuthContext,AuthContext' + + # Should NOT output deviceCode when secret is found + $output = Get-Content $env:GITHUB_OUTPUT -Raw + $output | Should -Not -Match "deviceCode=" + } + + It 'Should initiate device login when no secret is found' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{}' + + # Import AL-Go-Helper to get the functions defined, then mock them + . (Join-Path $scriptRoot "..\AL-Go-Helper.ps1") + Mock DownloadAndImportBcContainerHelper { } + Mock New-BcAuthContext { + return @{ deviceCode = "TESTDEVICECODE"; message = "Enter code to authenticate" } + } + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'nonExistentSecret' + + # Should invoke New-BcAuthContext to get device code + Should -Invoke New-BcAuthContext -Exactly -Times 1 + + # Should output deviceCode when no secret is found + $output = Get-Content $env:GITHUB_OUTPUT -Raw + $output | Should -Match "deviceCode=TESTDEVICECODE" + + # Should write device login message to step summary + $summary = Get-Content $env:GITHUB_STEP_SUMMARY -Raw + $summary | Should -Match "could not locate a secret" + } + + It 'Should output error when New-BcAuthContext returns null' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{}' + + . (Join-Path $scriptRoot "..\AL-Go-Helper.ps1") + Mock DownloadAndImportBcContainerHelper { } + Mock New-BcAuthContext { return $null } + Mock OutputError { } + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'nonExistentSecret' + + Should -Invoke OutputError -ParameterFilter { $message -like "*Failed to acquire authentication*" } + } + + It 'Should use environment-specific message when environmentName is provided' { + $env:Settings = '{"adminCenterApiCredentialsSecretName": "adminCenterApiCredentials"}' + $env:Secrets = '{}' + + . (Join-Path $scriptRoot "..\AL-Go-Helper.ps1") + Mock DownloadAndImportBcContainerHelper { } + Mock New-BcAuthContext { + return @{ deviceCode = "TESTDEVICECODE"; message = "Enter code to authenticate" } + } + + Invoke-Expression $actionScript + CheckAuthContext -secretName 'secret1,secret2' -environmentName 'MyEnv (Production)' + + $summary = Get-Content $env:GITHUB_STEP_SUMMARY -Raw + $summary | Should -Match "Business Central Environment MyEnv" + $summary | Should -Match "secret1 or secret2" + } +}