diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml index 6e38fb890..986fac1ed 100644 --- a/.github/workflows/E2E.yaml +++ b/.github/workflows/E2E.yaml @@ -182,7 +182,7 @@ jobs: Write-Host "scenarios=$scenariosJson" Scenario: - runs-on: [ ubuntu-latest ] + runs-on: [ windows-latest ] needs: [ Check, SetupRepositories, Analyze ] if: github.event.inputs.runScenarios == 'true' strategy: ${{ fromJson(needs.Analyze.outputs.scenarios) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56389e701..5e1de883d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: rev: 0.7.17 hooks: - id: mdformat - args: [--end-of-line=crlf] + args: [--end-of-line=keep] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 diff --git a/Actions/AL-Go-Helper.ps1 b/Actions/AL-Go-Helper.ps1 index b518e4e28..bf3f7034d 100644 --- a/Actions/AL-Go-Helper.ps1 +++ b/Actions/AL-Go-Helper.ps1 @@ -1269,21 +1269,25 @@ function GetProjectFolders { $projectFolders } -function installModules { +function InstallModule { Param( - [String[]] $modules + [String] $name, + [System.Version] $minimumVersion = $null ) - $modules | ForEach-Object { - if (-not (get-installedmodule -Name $_ -ErrorAction SilentlyContinue)) { - Write-Host "Installing module $_" - Install-Module $_ -Force | Out-Null - } + if ($null -eq $minimumVersion) { + $minimumVersion = [System.Version](GetPackageVersion -packageName $name) + } + $module = Get-Module -name $name -ListAvailable | Select-Object -First 1 + if ($module -and $module.Version -ge $minimumVersion) { + Write-Host "Module $name is available in version $($module.Version)" } - $modules | ForEach-Object { - Write-Host "Importing module $_" - Import-Module $_ -DisableNameChecking -WarningAction SilentlyContinue | Out-Null + else { + Write-Host "Installing module $name (minimum version $minimumVersion)" + Install-Module -Name $name -MinimumVersion "$minimumVersion" -Force | Out-Null } + Write-Host "Importing module $name (minimum version $minimumVersion)" + Import-Module -Name $name -MinimumVersion $minimumVersion -DisableNameChecking -WarningAction SilentlyContinue | Out-Null } function CloneIntoNewFolder { @@ -1329,7 +1333,7 @@ function CommitFromNewFolder { Param( [string] $serverUrl, [string] $commitMessage, - [string] $body = '', + [string] $body = $commitMessage, [string] $branch ) @@ -1664,7 +1668,7 @@ function CreateDevEnv { if (($settings.keyVaultName) -and -not ($bcAuthContext)) { Write-Host "Reading Key Vault $($settings.keyVaultName)" - installModules -modules @('Az.KeyVault') + InstallAzModuleIfNeeded -name 'Az.KeyVault' if ($kind -eq "local") { $LicenseFileSecret = Get-AzKeyVaultSecret -VaultName $settings.keyVaultName -Name $settings.licenseFileUrlSecretName @@ -2357,14 +2361,80 @@ function GetProjectsFromRepository { return @(GetMatchingProjects -projects $projects -selectProjects $selectProjects) } -function Get-PackageVersion($PackageName) { +function GetPackageVersion($packageName) { $alGoPackages = Get-Content -Path "$PSScriptRoot\Packages.json" | ConvertFrom-Json # Check if the package is in the list of packages - if ($alGoPackages.PSobject.Properties.name -match $PackageName) { - return $alGoPackages.$PackageName + if ($alGoPackages.PSobject.Properties.name -eq $PackageName) { + return $alGoPackages."$PackageName" } else { throw "Package $PackageName is not in the list of packages" } } + +function InstallAzModuleIfNeeded { + Param( + [string] $name, + [System.version] $minimumVersion = $null + ) + + if ($null -eq $minimumVersion) { + $minimumVersion = [System.Version](GetPackageVersion -packageName $name) + } + $azModule = Get-Module -Name $name + if ($azModule -and $azModule.Version -ge $minimumVersion) { + # Already installed + return + } + # GitHub hosted Linux runners have AZ PowerShell module saved in /usr/share/powershell/Modules/Az.* + if ($isWindows) { + # GitHub hosted Windows Runners have AzureRm PowerShell modules installed (deprecated) + # GitHub hosted Windows Runners have AZ PowerShell module saved in C:\Modules\az_* + # Remove AzureRm modules from PSModulePath and add AZ modules + if (Test-Path 'C:\Modules\az_*') { + $azModulesPath = Get-ChildItem 'C:\Modules\az_*' | Where-Object { $_.PSIsContainer } + if ($azModulesPath) { + Write-Host "Adding AZ module path: $($azModulesPath.FullName)" + $ENV:PSModulePath = "$($azModulesPath.FullName);$(("$ENV:PSModulePath".Split(';') | Where-Object { $_ -notlike 'C:\\Modules\Azure*' }) -join ';')" + } + } + } + InstallModule -name $name -minimumVersion $minimumVersion +} + +function ConnectAz { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'GitHub Secrets come in as plain text')] + param( + [PsCustomObject] $azureCredentials + ) + try { + Clear-AzContext -Scope Process + Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue + if ($azureCredentials.PSObject.Properties.Name -eq 'ClientSecret' -and $azureCredentials.ClientSecret) { + Write-Host "Connecting to Azure using clientId and clientSecret." + $credential = New-Object pscredential -ArgumentList $azureCredentials.ClientId, (ConvertTo-SecureString -string $azureCredentials.ClientSecret -AsPlainText -Force) + Connect-AzAccount -ServicePrincipal -Tenant $azureCredentials.TenantId -Credential $credential -WarningAction SilentlyContinue | Out-Null + } + else { + try { + Write-Host "Query federated token" + $result = Invoke-RestMethod -Method GET -UseBasicParsing -Headers @{ "Authorization" = "bearer $ENV:ACTIONS_ID_TOKEN_REQUEST_TOKEN"; "Accept" = "application/vnd.github+json" } -Uri "$ENV:ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" + } + catch { + throw "Unable to get federated token, maybe id_token: write permissions are missing. Error was $($_.Exception.Message)" + } + Write-Host "Connecting to Azure using clientId and federated token." + Connect-AzAccount -ApplicationId $azureCredentials.ClientId -Tenant $azureCredentials.TenantId -FederatedToken $result.value -WarningAction SilentlyContinue | Out-Null + } + if ($azureCredentials.PSObject.Properties.Name -eq 'SubscriptionId' -and $azureCredentials.SubscriptionId) { + Write-Host "Selecting subscription $($azureCredentials.SubscriptionId)" + Set-AzContext -SubscriptionId $azureCredentials.SubscriptionId -Tenant $azureCredentials.TenantId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null + } + $script:keyvaultConnectionExists = $true + Write-Host "Successfully connected to Azure" + } + catch { + throw "Error trying to authenticate to Azure. Error was $($_.Exception.Message)" + } +} diff --git a/Actions/Deliver/Deliver.ps1 b/Actions/Deliver/Deliver.ps1 index 65389e999..9b1f1bc35 100644 --- a/Actions/Deliver/Deliver.ps1 +++ b/Actions/Deliver/Deliver.ps1 @@ -20,31 +20,49 @@ [bool] $goLive ) -$telemetryScope = $null - -function EnsureAzStorageModule() { - if (get-command New-AzStorageContext -ErrorAction SilentlyContinue) { - Write-Host "Using Az.Storage PowerShell module" +function ConnectAzStorageAccount { + Param( + [PSCustomObject] $storageAccountCredentials + ) + + $azStorageContext = $null + if ($storageAccountCredentials.PSObject.Properties.Name -eq 'sastoken') { + try { + Write-Host "Creating AzStorageContext based on StorageAccountName and sastoken" + $azStorageContext = New-AzStorageContext -StorageAccountName $storageAccountCredentials.StorageAccountName -SasToken $storageAccountCredentials.sastoken + } + catch { + throw "Unable to create AzStorageContext based on StorageAccountName and sastoken. Error was: $($_.Exception.Message)" + } } - else { - $azureStorageModule = Get-Module -name 'Azure.Storage' -ListAvailable | Select-Object -First 1 - if ($azureStorageModule) { - Write-Host "Azure.Storage Module is available in version $($azureStorageModule.Version)" - Write-Host "Using Azure.Storage version $($azureStorageModule.Version)" - Import-Module 'Azure.Storage' -DisableNameChecking -WarningAction SilentlyContinue | Out-Null - Set-Alias -Name New-AzStorageContext -Value New-AzureStorageContext -Scope Script - Set-Alias -Name Get-AzStorageContainer -Value Get-AzureStorageContainer -Scope Script - Set-Alias -Name New-AzStorageContainer -Value New-AzureStorageContainer -Scope Script - Set-Alias -Name Set-AzStorageBlobContent -Value Set-AzureStorageBlobContent -Scope Script + elseif ($storageAccountCredentials.PSObject.Properties.Name -eq 'StorageAccountKey') { + try { + Write-Host "Creating AzStorageContext based on StorageAccountName and StorageAccountKey" + $azStorageContext = New-AzStorageContext -StorageAccountName $storageAccountCredentials.StorageAccountName -StorageAccountKey $storageAccountCredentials.StorageAccountKey } - else { - Write-Host "Installing and importing Az.Storage." - Install-Module 'Az.Storage' -Force - Import-Module 'Az.Storage' -DisableNameChecking -WarningAction SilentlyContinue | Out-Null + catch { + throw "Unable to create AzStorageContext based on StorageAccountName and StorageAccountKey. Error was: $($_.Exception.Message)" } } + elseif (($storageAccountCredentials.PSObject.Properties.Name -eq 'clientID') -and ($storageAccountCredentials.PSObject.Properties.Name -eq 'tenantID')) { + try { + InstallAzModuleIfNeeded -name 'Az.Accounts' + ConnectAz -azureCredentials $storageAccountCredentials + Write-Host "Creating AzStorageContext based on StorageAccountName and managed identity/app registration" + $azStorageContext = New-AzStorageContext -StorageAccountName $storageAccountCredentials.StorageAccountName -UseConnectedAccount + } + catch { + throw "Unable to create AzStorageContext based on StorageAccountName and managed identity. Error was: $($_.Exception.Message)" + } + } + else { + throw "Insufficient information in StorageContext secret. See https://aka.ms/algosettings#storagecontext for details" + } + return $azStorageContext } +$telemetryScope = $null + try { . (Join-Path -Path $PSScriptRoot -ChildPath "../AL-Go-Helper.ps1" -Resolve) DownloadAndImportBcContainerHelper @@ -315,35 +333,17 @@ try { Push-BcNuGetPackage -nuGetServerUrl $nuGetServerUrl -nuGetToken $nuGetToken -bcNuGetPackage $package } elseif ($deliveryTarget -eq "Storage") { - EnsureAzStorageModule + InstallAzModuleIfNeeded -name 'Az.Storage' try { - $storageAccount = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($secrets.storageContext)) | ConvertFrom-Json | ConvertTo-HashTable - # Check that containerName and blobName are present - $storageAccount.containerName | Out-Null - $storageAccount.blobName | Out-Null + $storageAccountCredentials = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($secrets.storageContext)) | ConvertFrom-Json + $storageAccountCredentials.StorageAccountName | Out-Null + $storageContainerName = $storageAccountCredentials.ContainerName.ToLowerInvariant().replace('{project}',$projectName).replace('{branch}',$refname).ToLowerInvariant() + $storageBlobName = $storageAccountCredentials.BlobName.ToLowerInvariant() } catch { - throw "StorageContext secret is malformed. Needs to be formatted as Json, containing StorageAccountName, containerName, blobName and sastoken or storageAccountKey.`nError was: $($_.Exception.Message)" - } - if ($storageAccount.Keys -contains 'sastoken') { - try { - $azStorageContext = New-AzStorageContext -StorageAccountName $storageAccount.StorageAccountName -SasToken $storageAccount.sastoken - } - catch { - throw "Unable to create AzStorageContext based on StorageAccountName and sastoken.`nError was: $($_.Exception.Message)" - } + throw "StorageContext secret is malformed. Needs to be formatted as Json, containing StorageAccountName, containerName, blobName.`nError was: $($_.Exception.Message)" } - else { - try { - $azStorageContext = New-AzStorageContext -StorageAccountName $storageAccount.StorageAccountName -StorageAccountKey $storageAccount.StorageAccountKey - } - catch { - throw "Unable to create AzStorageContext based on StorageAccountName and StorageAccountKey.`nError was: $($_.Exception.Message)" - } - } - - $storageContainerName = $storageAccount.ContainerName.ToLowerInvariant().replace('{project}',$projectName).replace('{branch}',$refname).ToLowerInvariant() - $storageBlobName = $storageAccount.BlobName.ToLowerInvariant() + $azStorageContext = ConnectAzStorageAccount -storageAccountCredentials $storageAccountCredentials Write-Host "Storage Container Name is $storageContainerName" Write-Host "Storage Blob Name is $storageBlobName" @@ -360,7 +360,7 @@ try { New-AzStorageContainer -Context $azStorageContext -Name $storageContainerName | Out-Null } - Write-Host "Delivering to $storageContainerName in $($storageAccount.StorageAccountName)" + Write-Host "Delivering to $storageContainerName in $($storageAccountCredentials.StorageAccountName)" $atypes.Split(',') | ForEach-Object { $atype = $_ Write-Host "Looking for: $project-$refname-$atype-*.*.*.*" @@ -415,7 +415,8 @@ try { # if type is Release, we only get here with the projects that needs to be delivered to AppSource # if type is CD, we get here for all projects, but should only deliver to AppSource if AppSourceContinuousDelivery is set to true if ($type -eq 'Release' -or $projectSettings.deliverToAppSource.continuousDelivery) { - EnsureAzStorageModule + # AppSource submission requires the Az.Storage module + InstallAzModuleIfNeeded -name 'Az.Storage' $appSourceContext = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($secrets.appSourceContext)) | ConvertFrom-Json | ConvertTo-HashTable if (!$appSourceContext) { throw "appSourceContext secret is missing" diff --git a/Actions/Deploy/Deploy.ps1 b/Actions/Deploy/Deploy.ps1 index 2a5dfca8e..02e55153e 100644 --- a/Actions/Deploy/Deploy.ps1 +++ b/Actions/Deploy/Deploy.ps1 @@ -20,16 +20,6 @@ $deploymentSettings = $deploymentEnvironments."$environmentName" $envName = $environmentName.Split(' ')[0] $secrets = $env:Secrets | ConvertFrom-Json -# Check obsolete secrets -"$($envName)-EnvironmentName","$($envName)_EnvironmentName","EnvironmentName" | ForEach-Object { - if ($secrets."$_") { - throw "The secret $_ is obsolete and should be replaced by using the EnvironmentName property in the DeployTo$envName setting in .github/AL-Go-Settings.json instead" - } -} -if ($secrets.Projects) { - throw "The secret Projects is obsolete and should be replaced by using the Projects property in the DeployTo$envName setting in .github/AL-Go-Settings.json instead" -} - $authContext = $null foreach($secretName in "$($envName)-AuthContext","$($envName)_AuthContext","AuthContext") { if ($secrets."$secretName") { @@ -110,7 +100,7 @@ else { try { $sandboxEnvironment = ($response.environmentType -eq 1) - if ($sandboxEnvironment -and !($bcAuthContext.ClientSecret)) { + if ($sandboxEnvironment -and !($bcAuthContext.ClientSecret -or $bcAuthContext.ClientAssertion)) { # Sandbox and not S2S -> use dev endpoint (Publish-BcContainerApp) $parameters = @{ "bcAuthContext" = $bcAuthContext diff --git a/Actions/DetermineDeliveryTargets/DetermineDeliveryTargets.ps1 b/Actions/DetermineDeliveryTargets/DetermineDeliveryTargets.ps1 index d6e148ef5..3988aee2d 100644 --- a/Actions/DetermineDeliveryTargets/DetermineDeliveryTargets.ps1 +++ b/Actions/DetermineDeliveryTargets/DetermineDeliveryTargets.ps1 @@ -8,7 +8,7 @@ function IncludeBranch([string] $deliveryTarget) { $settingsName = "DeliverTo$deliveryTarget" if ($settings.Contains($settingsName) -and $settings."$settingsName".Contains('Branches')) { - Write-Host "- Branches defined: $($settings."$settingsName".Branches -join ', ') - " + Write-Host "- Branches defined: $($settings."$settingsName".Branches -join ', ')" return ($null -ne ($settings."$settingsName".Branches | Where-Object { $ENV:GITHUB_REF_NAME -like $_ })) } else { @@ -18,7 +18,7 @@ function IncludeBranch([string] $deliveryTarget) { } function IncludeDeliveryTarget([string] $deliveryTarget) { - Write-Host "DeliveryTarget $_ - " + Write-Host "DeliveryTarget $_" # DeliveryTarget Context Secret needs to be specified for a delivery target to be included $contextName = "$($_)Context" $secrets = $env:Secrets | ConvertFrom-Json @@ -38,7 +38,7 @@ if ($settings.type -eq "AppSource App") { ($projectsJson | ConvertFrom-Json) | ForEach-Object { $projectSettings = ReadSettings -project $_ if ($projectSettings.deliverToAppSource.ContinuousDelivery -or ($projectSettings.Contains('AppSourceContinuousDelivery') -and $projectSettings.AppSourceContinuousDelivery)) { - Write-Host "Project $_ is setup for Continuous Delivery" + Write-Host "Project $_ is setup for Continuous Delivery to AppSource" $deliveryTargets += @("AppSource") } } diff --git a/Actions/Packages.json b/Actions/Packages.json index 62f06803c..feb8783e3 100644 --- a/Actions/Packages.json +++ b/Actions/Packages.json @@ -1,3 +1,6 @@ { - "sign": "0.9.1-beta.24123.2" + "sign": "0.9.1-beta.24123.2", + "Az.Accounts": "2.15.1", + "Az.Storage": "6.1.1", + "Az.KeyVault": "5.2.0" } diff --git a/Actions/ReadSecrets/ReadSecrets.ps1 b/Actions/ReadSecrets/ReadSecrets.ps1 index ff23b3c85..e83099a22 100644 --- a/Actions/ReadSecrets/ReadSecrets.ps1 +++ b/Actions/ReadSecrets/ReadSecrets.ps1 @@ -100,6 +100,17 @@ try { MaskValue -key "$($secretName).$($keyName)" -value "$($json."$keyName")" } } + if ($json.ContainsKey('clientID') -and !($json.ContainsKey('clientSecret') -or $json.ContainsKey('refreshToken'))) { + try { + Write-Host "Query federated token" + $result = Invoke-RestMethod -Method GET -UseBasicParsing -Headers @{ "Authorization" = "bearer $ENV:ACTIONS_ID_TOKEN_REQUEST_TOKEN"; "Accept" = "application/vnd.github+json" } -Uri "$ENV:ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange" + $json += @{ "clientAssertion" = $result.value } + $secretValue = $json | ConvertTo-Json -Compress + } + catch { + throw "$SecretName doesn't contain any ClientSecret and AL-Go is unable to acquire an ID_TOKEN. Error was $($_.Exception.Message)" + } + } } $base64value = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($secretValue)) $outSecrets += @{ "$secretsProperty" = $base64value } diff --git a/Actions/ReadSecrets/ReadSecretsHelper.psm1 b/Actions/ReadSecrets/ReadSecretsHelper.psm1 index f7b5e887c..1c02befc9 100644 --- a/Actions/ReadSecrets/ReadSecretsHelper.psm1 +++ b/Actions/ReadSecrets/ReadSecretsHelper.psm1 @@ -5,13 +5,10 @@ Param( $script:gitHubSecrets = $_gitHubSecrets | ConvertFrom-Json $script:keyvaultConnectionExists = $false -$script:azureRm210 = $false $script:isKeyvaultSet = $script:gitHubSecrets.PSObject.Properties.Name -eq "AZURE_CREDENTIALS" $script:escchars = @(' ','!','\"','#','$','%','\u0026','\u0027','(',')','*','+',',','-','.','/','0','1','2','3','4','5','6','7','8','9',':',';','\u003c','=','\u003e','?','@','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','[','\\',']','^','_',[char]96,'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','{','|','}','~') -function IsKeyVaultSet { - return $script:isKeyvaultSet -} +. (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) function MaskValue { Param( @@ -78,12 +75,12 @@ function GetKeyVaultCredentials { } try { $creds = $jsonStr | ConvertFrom-Json - # Mask ClientSecret - MaskValue -key 'clientSecret' -value $creds.ClientSecret - $creds.ClientSecret = ConvertTo-SecureString $creds.ClientSecret -AsPlainText -Force + if ($creds.PSObject.Properties.Name -eq 'ClientSecret' -and $creds.ClientSecret) { + # Mask ClientSecret + MaskValue -key 'ClientSecret' -value $creds.ClientSecret + } # Check thet $creds contains the needed properties $creds.ClientId | Out-Null - $creds.subscriptionId | Out-Null $creds.TenantId | Out-Null } catch { @@ -108,75 +105,6 @@ function GetKeyVaultCredentials { return $creds } -function InstallKeyVaultModuleIfNeeded { - if ($isWindows -and (Test-Path 'C:\Modules\az_*')) { - $azModulesPath = Get-ChildItem 'C:\Modules\az_*' | Where-Object { $_.PSIsContainer } - if ($azModulesPath) { - Write-Host $azModulesPath.FullName - $ENV:PSModulePath = "$($azModulesPath.FullName);$(("$ENV:PSModulePath".Split(';') | Where-Object { $_ -notlike 'C:\\Modules\Azure*' }) -join ';')" - } - } - - $azKeyVaultModule = Get-Module -name 'Az.KeyVault' -ListAvailable | Select-Object -First 1 - if ($azKeyVaultModule) { - Write-Host "Az.KeyVault Module is available in version $($azKeyVaultModule.Version)" - Write-Host "Using Az.KeyVault version $($azKeyVaultModule.Version)" - Import-Module 'Az.KeyVault' -DisableNameChecking -WarningAction SilentlyContinue | Out-Null - } - else { - $AzKeyVaultModule = Get-InstalledModule -Name 'Az.KeyVault' -ErrorAction SilentlyContinue - if ($AzKeyVaultModule) { - Write-Host "Az.KeyVault version $($AzKeyVaultModule.Version) is installed" - Import-Module 'Az.KeyVault' -DisableNameChecking -WarningAction SilentlyContinue - } - else { - $azureRmKeyVaultModule = Get-Module -name 'AzureRm.KeyVault' -ListAvailable | Select-Object -First 1 - if ($azureRmKeyVaultModule) { Write-Host "AzureRm.KeyVault Module is available in version $($azureRmKeyVaultModule.Version)" } - $azureRmProfileModule = Get-Module -name 'AzureRm.Profile' -ListAvailable | Select-Object -First 1 - if ($azureRmProfileModule) { Write-Host "AzureRm.Profile Module is available in version $($azureRmProfileModule.Version)" } - if ($azureRmKeyVaultModule -and $azureRmProfileModule -and "$($azureRmKeyVaultModule.Version)" -eq "2.1.0" -and "$($azureRmProfileModule.Version)" -eq "2.1.0") { - Write-Host "Using AzureRM version 2.1.0" - $script:azureRm210 = $true - $azureRmKeyVaultModule | Import-Module -WarningAction SilentlyContinue - $azureRmProfileModule | Import-Module -WarningAction SilentlyContinue - Disable-AzureRmDataCollection -WarningAction SilentlyContinue - } - else { - Write-Host "Installing and importing Az.KeyVault." - Install-Module 'Az.KeyVault' -Force - Import-Module 'Az.KeyVault' -DisableNameChecking -WarningAction SilentlyContinue | Out-Null - } - } - } -} - -function ConnectAzureKeyVault { - param( - [string] $subscriptionId, - [string] $tenantId, - [string] $clientId, - [SecureString] $clientSecret - ) - try { - $credential = New-Object PSCredential -argumentList $clientId, $clientSecret - if ($script:azureRm210) { - Add-AzureRmAccount -ServicePrincipal -Tenant $tenantId -Credential $credential -WarningAction SilentlyContinue | Out-Null - Set-AzureRmContext -SubscriptionId $subscriptionId -Tenant $tenantId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null - } - else { - Clear-AzContext -Scope Process - Clear-AzContext -Scope CurrentUser -Force -ErrorAction SilentlyContinue - Connect-AzAccount -ServicePrincipal -Tenant $tenantId -Credential $credential -WarningAction SilentlyContinue | Out-Null - Set-AzContext -SubscriptionId $subscriptionId -Tenant $tenantId -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null - } - $script:keyvaultConnectionExists = $true - Write-Host "Successfully connected to Azure Key Vault." - } - catch { - throw "Error trying to authenticate to Azure using Az. Error was $($_.Exception.Message)" - } -} - function GetKeyVaultSecret { param ( [string] $secretName, @@ -189,8 +117,13 @@ function GetKeyVaultSecret { } if (-not $script:keyvaultConnectionExists) { - InstallKeyVaultModuleIfNeeded - ConnectAzureKeyVault -subscriptionId $keyVaultCredentials.subscriptionId -tenantId $keyVaultCredentials.tenantId -clientId $keyVaultCredentials.clientId -clientSecret $keyVaultCredentials.clientSecret + InstallAzModuleIfNeeded -name 'Az.KeyVault' + try { + ConnectAz -azureCredentials $keyVaultCredentials + } + catch { + throw "Error trying to get secrets from Azure Key Vault. Error was $($_.Exception.Message)" + } } $secretSplit = $secretName.Split('=') @@ -201,11 +134,16 @@ function GetKeyVaultSecret { } $value = $null - if ($script:azureRm210) { - $keyVaultSecret = Get-AzureKeyVaultSecret -VaultName $keyVaultCredentials.keyVaultName -Name $secret -ErrorAction SilentlyContinue + try { + $keyVaultSecret = Get-AzKeyVaultSecret -VaultName $keyVaultCredentials.keyVaultName -Name $secret } - else { - $keyVaultSecret = Get-AzKeyVaultSecret -VaultName $keyVaultCredentials.keyVaultName -Name $secret -ErrorAction SilentlyContinue + catch { + if ($keyVaultCredentials.PSObject.Properties.Name -eq 'ClientSecret') { + throw "Error trying to get secrets from Azure Key Vault. Error was $($_.Exception.Message)" + } + else { + throw "Error trying to get secrets from Azure Key Vault, maybe your Key Vault isn't setup for role based access control?. Error was $($_.Exception.Message)" + } } if ($keyVaultSecret) { if ($encrypted) { diff --git a/Actions/Sign/Sign.ps1 b/Actions/Sign/Sign.ps1 index 0982fbeb5..2fa63ec0d 100644 --- a/Actions/Sign/Sign.ps1 +++ b/Actions/Sign/Sign.ps1 @@ -45,15 +45,25 @@ try { else { throw "KeyVaultName is not specified in AzureCredentials nor in settings. Please specify it in one of them." } + + $AzureCredentialParams = @{ + "ClientId" = $AzureCredentials.clientId + "TenantId" = $AzureCredentials.tenantId + } + if ($AzureCredentials.PSobject.Properties.name -eq "clientSecret") { + $AzureCredentialParams += @{ + "ClientSecret" = $AzureCredentials.clientSecret + } + } + InstallAzModuleIfNeeded -name 'Az.Accounts' + ConnectAz -azureCredentials $AzureCredentialParams + $description = "Signed with AL-Go for GitHub" $descriptionUrl = "$ENV:GITHUB_SERVER_URL/$ENV:GITHUB_REPOSITORY" Write-Host "::group::Signing files" - Invoke-SigningTool -KeyVaultName $AzureKeyVaultName ` + Invoke-SigningTool @AzureCredentialParams -KeyVaultName $AzureKeyVaultName ` -CertificateName $settings.keyVaultCodesignCertificateName ` - -ClientId $AzureCredentials.clientId ` - -ClientSecret $AzureCredentials.clientSecret ` - -TenantId $AzureCredentials.tenantId ` -FilesToSign $PathToFiles ` -Description $description ` -DescriptionUrl $descriptionUrl ` diff --git a/Actions/Sign/Sign.psm1 b/Actions/Sign/Sign.psm1 index 7cf93a226..f7207d5d2 100644 --- a/Actions/Sign/Sign.psm1 +++ b/Actions/Sign/Sign.psm1 @@ -5,22 +5,22 @@ Installs the dotnet signing tool. #> function Install-SigningTool() { - . (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) + . (Join-Path -Path $PSScriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) - # Create folder in temp directory with a unique name - $tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) "SigningTool-$(Get-Random)" + # Create folder in temp directory with a unique name + $tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) "SigningTool-$(Get-Random)" - # Get version of the signing tool - $version = Get-PackageVersion -PackageName "sign" + # Get version of the signing tool + $version = GetPackageVersion -PackageName "sign" - # Install the signing tool in the temp folder - Write-Host "Installing signing tool version $version in $tempFolder" - New-Item -ItemType Directory -Path $tempFolder | Out-Null - dotnet tool install sign --version $version --tool-path $tempFolder | Out-Null + # Install the signing tool in the temp folder + Write-Host "Installing signing tool version $version in $tempFolder" + New-Item -ItemType Directory -Path $tempFolder | Out-Null + dotnet tool install sign --version $version --tool-path $tempFolder | Out-Null - # Return the path to the signing tool - $signingTool = Join-Path -Path $tempFolder "sign.exe" -Resolve - return $signingTool + # Return the path to the signing tool + $signingTool = Join-Path -Path $tempFolder "sign.exe" -Resolve + return $signingTool } <# @@ -28,16 +28,17 @@ function Install-SigningTool() { Signs files in a given path using a certificate from Azure Key Vault. .DESCRIPTION Signs files in a given path using a certificate from Azure Key Vault. + Connection to the Azure Key Vault can be done using a service principal or a managed identity. .PARAMETER KeyVaultName The name of the Azure Key Vault where the certificate is stored. .PARAMETER CertificateName The name of the certificate in the Azure Key Vault. .PARAMETER ClientId - The client ID of the service principal used to authenticate with Azure Key Vault. + [Optional] The client ID of the service principal used to authenticate with Azure Key Vault. If not specified, managed identity will be used. .PARAMETER ClientSecret - The client secret of the service principal used to authenticate with Azure Key Vault. + [Optional] The client secret of the service principal used to authenticate with Azure Key Vault. If not specified, managed identity will be used. .PARAMETER TenantId - The tenant ID of the service principal used to authenticate with Azure Key Vault. + [Optional] The tenant ID of the service principal used to authenticate with Azure Key Vault. If not specified, managed identity will be used. .PARAMETER FilesToSign The path to the file(s) to be signed. Supports wildcards. .PARAMETER Description @@ -51,8 +52,10 @@ function Install-SigningTool() { .PARAMETER Verbosity The verbosity level of the signing tool. .EXAMPLE - Invoke-SigningTool -KeyVaultName "my-key-vault" -CertificateName "my-certificatename" -ClientId "my-client-id" -ClientSecret "my-client-secret" -TenantId "my-tenant-id" + Invoke-SigningTool -KeyVaultName "my-key-vault" -CertificateName "my-certificatename" -ClientId "my-client-id" -ClientSecret "my-client-secret" -TenantId "my-tenant-id" ` -FilesToSign "C:\path\to\files\*.app" -Description "Signed with AL-Go for GitHub" -DescriptionUrl "github.com/myorg/myrepo" + .EXAMPLE + Invoke-SigningTool -KeyVaultName "my-key-vault" -CertificateName "my-certificatename" -FilesToSign "C:\path\to\files\*.app" -Description "Signed with AL-Go for GitHub" -DescriptionUrl "github.com/myorg/myrepo" #> function Invoke-SigningTool() { param( @@ -60,11 +63,11 @@ function Invoke-SigningTool() { [string] $KeyVaultName, [Parameter(Mandatory = $true)] [string] $CertificateName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string] $ClientId, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string] $ClientSecret, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string] $TenantId, [Parameter(Mandatory = $true)] [string] $FilesToSign, @@ -83,19 +86,36 @@ function Invoke-SigningTool() { $signingToolExe = Install-SigningTool # Sign files - . $signingToolExe code azure-key-vault ` - --azure-key-vault-url "https://$KeyVaultName.vault.azure.net/" ` - --azure-key-vault-certificate $CertificateName ` - --azure-key-vault-client-id $ClientId ` - --azure-key-vault-client-secret $ClientSecret ` - --azure-key-vault-tenant-id $TenantId ` - --description $Description ` - --description-url $DescriptionUrl ` - --file-digest $DigestAlgorithm ` - --timestamp-digest $DigestAlgorithm ` - --timestamp-url $TimestampService ` - --verbosity $Verbosity ` - $FilesToSign + if ($ClientId -and $ClientSecret -and $TenantId) { + Write-Host "Invoking signing tool using clientId/clientSecret" + . $signingToolExe code azure-key-vault ` + --azure-key-vault-url "https://$KeyVaultName.vault.azure.net/" ` + --azure-key-vault-certificate $CertificateName ` + --azure-key-vault-client-id $ClientId ` + --azure-key-vault-client-secret $ClientSecret ` + --azure-key-vault-tenant-id $TenantId ` + --description $Description ` + --description-url $DescriptionUrl ` + --file-digest $DigestAlgorithm ` + --timestamp-digest $DigestAlgorithm ` + --timestamp-url $TimestampService ` + --verbosity $Verbosity ` + $FilesToSign + } + else { + Write-Host "Invoking signing tool using managed identity" + . $signingToolExe code azure-key-vault ` + --azure-key-vault-url "https://$KeyVaultName.vault.azure.net/" ` + --azure-key-vault-certificate $CertificateName ` + --azure-key-vault-managed-identity $true ` + --description $Description ` + --description-url $DescriptionUrl ` + --file-digest $DigestAlgorithm ` + --timestamp-digest $DigestAlgorithm ` + --timestamp-url $TimestampService ` + --verbosity $Verbosity ` + $FilesToSign + } } Export-ModuleMember -Function Invoke-SigningTool diff --git a/CODEOWNERS b/CODEOWNERS index 46f692830..d27eac40d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1,2 @@ -# These owners will be the default owners for everything in the repo. -* @microsoft/d365-bc-engineering-systems +# These owners will be the default owners for everything in the repo. +* @microsoft/d365-bc-engineering-systems diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8080f9ce2..d71c8946f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,14 @@ - Issue 1105 Increment Version Number - repoVersion in .github/AL-Go-Settings.json is not updated - Issue 1073 Publish to AppSource - Automated validation: failure +### Dependencies to PowerShell modules + +AL-Go for GitHub relies on specific PowerShell modules, and the minimum versions required for these modules are tracked in [Packages.json](https://raw.githubusercontent.com/microsoft/AL-Go/main/Actions/Packages.json) file. Should the installed modules on the GitHub runner not meet these minimum requirements, the necessary modules will be installed as needed. + +### Support managed identities and federated credentials + +All authentication context secrets now supports managed identities and federated credentials. See more [here](Scenarios/secrets.md). Furthermore, you can now use https://aka.ms/algosecrets#authcontext to learn more about the formatting of that secret. + ### Business Central Performance Toolkit Test Result Viewer In the summary after a Test Run, you now also have the result of performance tests. diff --git a/Scenarios/Codesigning.md b/Scenarios/Codesigning.md index a240f2e16..68c340cf3 100644 --- a/Scenarios/Codesigning.md +++ b/Scenarios/Codesigning.md @@ -13,11 +13,19 @@ This guide will take you through how to set up your AL-Go project with an Azure How you do this might depend on which Certificate Authority you are getting your certificate from. DigiCert and GlobalSign have integrations with Azure Key Vault. You can follow [this guide](https://learn.microsoft.com/en-us/azure/key-vault/certificates/how-to-integrate-certificate-authority) on how to set up that integration if you are using one of those CAs. Once you have set up the integration, you can request a certificate from within your Azure Key Vault. If you are using another CA you can try following this guide to [Generate a CSR and Install a Certificate in Microsoft Azure Key Vault](https://www.ssl.com/how-to/generate-csr-install-certificate-microsoft-azure-key-vault/). If neither of those options work for you, please engage with your CA to get the certificate into the Key Vault. -2. Configure an Azure Key Vault access policy for the service principal that will be used for signing. At minimum, the account needs the following permissions: +2. An Azure Key Vault can be set up for two different security models: Role Based Access Control (RBAC) (recommended) and Vault Access Policy. In order for AL-Go for GitHub to use the Key Vault, the following roles/permissions need to be assigned to the app registration or Managed Identity, on which the authentication is performed: + +Role Based Access Control, roles needed: + +- Key Vault Crypto User +- Key Vault Certificate User + +Vault Access Policy, permissions needed: - Cryptographic Operations: Sign -- Certificate Management Operations: Get - ![Key Vault Access Policies](https://github.com/microsoft/AL-Go/assets/117829001/c93375e0-ce5b-4aa0-a6b9-a845a87fddef) +- Certificate permissions: Get + +See more [here](https://aka.ms/algosecrets#azure_credentials). ## Setting up AL-Go for Code Signing diff --git a/Scenarios/secrets.md b/Scenarios/secrets.md new file mode 100644 index 000000000..a42636eba --- /dev/null +++ b/Scenarios/secrets.md @@ -0,0 +1,186 @@ +# Secrets + +The behavior of AL-Go for GitHub is very much controlled by settings and secrets. + +To learn more about the settings used by AL-Go for GitHub, please navigate to [Settings](settings.md). + +## Where are the secrets defined + +Secrets in GitHub can be defined on the Organizational level, on the repository level or on an environment. + +**Organizational secrets** are defined on your GitHub organization and can be shared with the repositories in your organization. For the free GitHub plan, organizational secrets can only be shared with public repositories. + +**Repository secrets** are defined on the individual repository and you can define any number of secrets on the repository. If you define a secret on the repository level with the same name as an organizational secret, shared with the repository, the repository secret overrides the organizational secret. + +**Environment secrets** are defined underneath an environment and is only available to the workflow during deployment to this environment. For the free GitHub plan, environments (and secrets obviously) are only available on public repositories. + +See also [https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#about-secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#about-secrets). + +> \[!NOTE\] +> In AL-Go for GitHub you can also define your secrets in an Azure KeyVault, but you would still need to create one secret in GitHub called [Azure_Credentials](https://aka.ms/algosecrets#azure_credentials) to be able to access the Azure KeyVault. + +## Important information about secrets (e.g. common mistakes...) + +Please read the following topics carefully and make sure that you do not run into any of these common mistakes, which might cause some problems. + +### Don't have secrets that are not secret + +All secrets exposed to a repository will be masked (i.e. replaced with \*\*\*) in the workflow logs of that repository, ensuring that secret values are not exposed to the public. In GitHub, secrets are also not allowed to be transferred between jobs. If a variable transferred between two jobs contains a secret, you will see warnings like this in the output: + +![image](https://github.com/microsoft/AL-Go/assets/10775043/b280360b-d3e8-47b9-8993-39b0de76d44a) + +In this case, a secret with the value "windows" have been created and since the Initialization step transfers the githubRunner to the Build steps with the value "windows-latest", this will break AL-Go for GitHub. + +So, don't have secrets that are not secrets as this might break core functionality in AL-Go for GitHub. + +### Use compressed JSON + +AL-Go for GitHub uses JSON structures for some secrets (like authentication contexts). AL-Go for GitHub will ensure that individual secret property values are masked in the log as well as the full JSON structure. When creating a JSON structure secret, it is important to use compressed JSON as GitHub will mask individual lines as well. This means that a non-compressed JSON structure will cause the curly bracket characters to be handled as secrets, breaking AL-Go for GitHub. In the logs you will see that the curly brackets are replaced with \*\*\* + +![image](https://github.com/microsoft/AL-Go/assets/10775043/58bbc120-f36d-499d-8e6c-8cc87f55d918) + +In this case, a secret is created with the following value: + +``` +{ + "prop": "value" +} +``` + +So, don't have multi-line secrets, where individual lines are not secrets as this might break core functionality in AL-Go for GitHub. + +### Only expose secrets that are necessary to your repositories that run on AL-Go for GitHub. + +If your GitHub organization might have many organizational secrets, please only allow access to the secrets actually used by your AL-Go for GitHub repository. If any of the secrets made available to your repository contains multi-line secrets or have secrets, where the value is not really a secret, it might break core functionality in AL-Go for GitHub. + +# Secrets + +## **Azure_Credentials** -> Connect to Azure + +By creating a secret called Azure_Credentials you can give your GitHub repository access to an Azure Key Vault, from which you can read secrets and use for managed signing of your apps. You can use a managed identity or an app registration (service to service) for authentication. + +> \[!NOTE\] +> In order to use a KeyVault for signing apps, it needs to be a premium SKU KeyVault. You can use this command to modify an existing KeyVault: `az keyvault update --set properties.sku.name=premium --name --resource-group ` + +n Azure Key Vault can be set up for two different security models: Role Based Access Control (RBAC) (recommended) and Vault Access Policy. In order for AL-Go for GitHub to use the Key Vault, the following roles/permissions need to be assigned to the app registration or Managed Identity, on which the authentication is performed: + +| Security Model | Read Secrets | Sign Apps | +| :-- | :-- | :-- | +| Role Based Access Control | Role: Key Vault Secrets User | Roles: Key Vault Crypto User + Key Vault Certificate User | +| Vault Access Policy | Secret permissions: Get, List | Cryptographic Operations: Sign + Certificate permissions: Get | + +### Managed Identity or App Registration + +Whether you use a managed identity or an app registration for authentication, you need to assign the right permissions / roles to its Client Id (Application Id). For managed identities, the only authentication mechanism supported is federated credentials. For an app registration you can use federated credentials or a client Secret. + +#### Federated credential + +Using a federated credential, you need to register your GitHub repository in your managed identity under settings -> federated credentials or in the app registration under Certificates & Secrets. This registration will allow AL-Go for GitHub running in this repository to authenticate without the Client Secret stored. You still need to create a secret containing the clientId and the tenantId. The way this works is that AL-Go for GitHub will request an ID_TOKEN from GitHub as a proof of authenticity and use this when authenticating. This way, only workflows running in the specified branch/environment in GitHub will be able to authenticate. + +Example: `{"keyVaultName":"MyKeyVault","clientId":"ed79570c-0384-4826-8099-bf0577af6667","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045"}` + +#### ClientSecret + +ClientSecret can only be used using an app registration. Under Certificates & Secrets in the app registration, you need to create a Client Secret, which you can specify in the AuthContext secret in AL-Go for GitHub. With the ClientId and ClientSecret, anybody can authenticate and perform actions as the connected user inside Business Central. + +Example: `{"keyVaultName":"MyKeyVault","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","clientSecret":"OPXxxxxxxxxxxxxxxxxxxxxxxabge","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045"}` + +With this setup, you can create a setting called `keyVaultCodesignCertificateName` containing the name of the imported certificate in your Key Vault in order for AL-Go for GitHub to sign your apps. + +## **AuthContext** -> Deploy to an environment + +Whenever AL-Go for GitHub is doing to deploy to an environment, it will need an AuthContext secret. The AuthContext secret can be provided underneath the environment in GitHub. If you are using a private repository in the free GitHub plan, you do not have environments. Then you can create an AuthContext secret in the repository. If you have multiple environments, you can create different AuthContext secrets by using the environment name followed by an underscore and AuthContext (f.ex. **QA_AuthContext**). + +### Managed identity + +Managed identities cannot be used for deploying to a Business Central environment as this is not an Azure resource + +### Impersonation/RefreshToken + +Specifying a RefreshToken allows AL-Go for GitHub to get access to impersonate the user who created the refresh token and act on behalf of that user on the scopes for which the refresh token was created. In this case, access is given to act as the user in Business Central. + +Providing an AuthContext secret with a refreshtoken typically allows you to get access for 90 days. After the 90 days, you need to refresh the AuthContext secret with a new refreshToken. Note that anybody with the refreshToken can get access to call the API on behalf of the user, it doesn't have to be inside a workflow/pipeline. + +Example: `{"tenantId":"d630ce39-5a0c-41ec-bf0d-6758ad558f0c","scopes":"https://api.businesscentral.dynamics.com/","RefreshToken":"0.AUUAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx_s6Eo4YOI","clientId":"1950a258-227b-4e31-a9cf-717495945fc2"}` + +### App Registration (Service to service authentication) + +In order to use an app registration for publishing apps to Business Central, you need to register the ClientId (Application Id) of is app registration inside Business Central. This will create a user inside Business Central and you need to give the following permissions to that user: D365 AUTOMATION and EXTEN. MGT. - ADMIN. After this, there are two ways you can authenticate, either using Federated credential or using a Client Secret. + +#### Federated credential + +Using a federated credential, you need to register your GitHub repository in the app registration under Certificates & Secrets. This registration will allow AL-Go for GitHub running in this repository to authenticate without the Client Secret stored. You still need to create a secret containing this information. The way this works is that AL-Go for GitHub will request an ID_TOKEN from GitHub as a proof of authenticity and use this when authenticating. This way, only workflows running in the specified branch/environment in GitHub will be able to authenticate. + +Example:`{"tenantId":"d630ce39-5a0c-41ec-bf0d-6758ad558f0c","scopes":"https://api.businesscentral.dynamics.com/","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32"}` + +#### Client Secret + +Under Certificates & Secrets in the app registration, you can create a Client Secret, which you can specify in the AuthContext secret in AL-Go for GitHub. With the ClientId and ClientSecret, anybody can authenticate and perform actions as the connected user inside Business Central. + +Example: `{"tenantId":"d630ce39-5a0c-41ec-bf0d-6758ad558f0c","scopes":"https://api.businesscentral.dynamics.com/","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","clientSecret":"OPXxxxxxxxxxxxxxxxxxxxxxxabge"}` + +## **AppSourceContext** -> Deliver to AppSource + +Adding a secret called AppSourceContext to an AL-Go for GitHub repository from the AppSource template, enables automatic delivery to AppSource. + +### Managed identity + +Managed identities cannot be used for submitting to AppSource as the partner center API is not an Azure resource + +### App Registration (Service to service authentication) + +In order to use an app registration for publishing apps to AppSource, you need to register the ClientId (Application Id) of is app registration in Partner Center. After this, there are two ways you can authenticate, either using Federated credential or using a Client Secret. + +#### Federated credential + +Using a federated credential, you need to register your GitHub repository in the app registration under Certificates & Secrets. This registration will allow AL-Go for GitHub running in this repository to authenticate without the Client Secret stored. You still need to create a secret containing this information. The way this works is that AL-Go for GitHub will request an ID_TOKEN from GitHub as a proof of authenticity and use this when authenticating. This way, only workflows running in the specified branch/environment in GitHub will be able to authenticate. + +Example:`{"clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045","scopes":"https://api.partner.microsoft.com/.default"}` + +#### Client Secret + +Under Certificates & Secrets in the app registration, you can create a Client Secret, which you can specify in the AuthContext secret in AL-Go for GitHub. Note that who ever has access to the clientId and clientSecret can publish apps on AppSource on your behalf. + +Example: `{"tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045","scopes":"https://api.partner.microsoft.com/.default","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","clientSecret":"OPXxxxxxxxxxxxxxxxxxxxxxxabge"}` + +## **StorageContext** -> Deliver to storage + +Adding a secret called StorageContext to an AL-Go for GitHub repository, enables automatic delivery to an Azure storage account. + +In AL-Go for GitHub, the Storage Context can be specified in 5 different ways, 5 different authentication mechanism towards an Azure Storage Account. + +### Managed Identity/Federated credential + +As a storage account is an Azure resource, we can use managed identities. Managed identities are like virtual users in Azure, using federated credentials for authentication. Using a federated credential, you need to register your GitHub repository in the managed identity under Settings -> Federated Credentials. The way this works is that AL-Go for GitHub will request an ID_TOKEN from GitHub as a proof of authenticity and use this when authenticating. This way, only workflows running in the specified branch/environment in GitHub will be able to authenticate. + +Example: `{"storageAccountName":"MyStorageName","clientId":"08b6d80c-68cf-48f9-a5ff-b054326e2ec3","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045","containerName":"{project}","blobName":"{version}/{project}-{type}.zip"}` + +### App Registration/Federated credential + +An app registration with federated credential is harder to setup than a managed identity, but just as secure. The mechanism is the same for obtaining an ID_TOKEN and providing this as proof of authenticity towards the app registration. + +Example: `{"storageAccountName":"MyStorageName","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045","containerName":"{project}","blobName":"{version}/{project}-{type}.zip"}` + +### App Registration/Client Secret + +An app registration with a client Secret is less secure than using federated credentials. Who ever has access to the clientSecret has access to everything the app registration has access to, until you recycle the client Secret. + +Example: `{"storageAccountName":"MyStorageName","clientId":"d48b773f-2c26-4394-8bd2-c5b64e0cae32","clientSecret":"OPXxxxxxxxxxxxxxxxxxxxxxxabge","tenantId":"c645f7e7-0613-4b82-88ca-71f3dbb40045","containerName":"{project}","blobName":"{version}/{project}-{type}.zip"}` + +### storageAccountName/sastoken + +A sas token for a storage account can be setup to function in a limited timeframe, giving access to perform a certain number of tasks on the storage account. Who ever has access to the sastoken can perform these tasks on the storage account until it expires or you recycle the storage account key used to create the sastoken. + +Example: `{"storageAccountName":"MyStorageName","sastoken":"sv=2022-11-02&ss=b&srt=sco&sp=rwdlaciytf&se=2024-08-06T20:22:08Z&st=2024-04-06T12:22:08Z&spr=https&sig=IZyIf5xxxxxxxxxxxxxxxxxxxxxtq7tj6b5I%3D","containerName":"{project}","blobName":"{version}/{project}-{type}.zip"}` + +### storageAccountName/storageAccountKey + +Using storageAccount Name and Key is by far the most unsecure way of authenticating to an Azure Storage Account. If ever compromised, people can do anything with these credentials, until the storageAccount key is cycled. + +Example: `{"storageAccountName":"MyStorageName","storageAccountKey":"JHFZErCyfQ8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxStj7AHXQ==","containerName":"{project}","blobName":"{version}/{project}-{type}.zip"} ` + +## **GitHubPackagesContext** -> Deliver to GitHub Packages + +If you create a secret called GitHubPackagesContext, then AL-Go for GitHub will automagically deliver apps to this NuGet feed after every successful build. AL-Go for GitHub will also use this NuGet feed for dependency resolution when building apps, giving you automatic dependency resolution within all your apps. + +Example: `{"token":"ghp_NDdI2ExxxxxxxxxxxxxxxxxAYQh","serverUrl":"https://nuget.pkg.github.com/mygithuborg/index.json"}` diff --git a/Scenarios/settings.md b/Scenarios/settings.md index 536875e9c..7ee02511e 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -1,6 +1,8 @@ # Settings -The behavior of AL-Go for GitHub is very much controlled by settings. +The behavior of AL-Go for GitHub is very much controlled by settings and secrets. + +To learn more about the secrets used by AL-Go for GitHub, please navigate to [Secrets](secrets.md). ## Where are the settings located diff --git a/Templates/AppSource App/.AL-Go/cloudDevEnv.ps1 b/Templates/AppSource App/.AL-Go/cloudDevEnv.ps1 index eca0288fd..750a65cae 100644 --- a/Templates/AppSource App/.AL-Go/cloudDevEnv.ps1 +++ b/Templates/AppSource App/.AL-Go/cloudDevEnv.ps1 @@ -12,6 +12,21 @@ Param( $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 +function DownloadHelperFile { + param( + [string] $url, + [string] $folder + ) + + $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' + $name = [System.IO.Path]::GetFileName($url) + Write-Host "Downloading $name from $url" + $path = Join-Path $folder $name + Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path + $ProgressPreference = $prevProgressPreference + return $path +} + try { Clear-Host Write-Host @@ -25,17 +40,11 @@ Write-Host -ForegroundColor Yellow @' '@ -$webClient = New-Object System.Net.WebClient -$webClient.CachePolicy = New-Object System.Net.Cache.RequestCachePolicy -argumentList ([System.Net.Cache.RequestCacheLevel]::NoCacheNoStore) -$webClient.Encoding = [System.Text.Encoding]::UTF8 -$GitHubHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -Write-Host "Downloading GitHub Helper module from $GitHubHelperUrl" -$GitHubHelperPath = "$([System.IO.Path]::GetTempFileName()).psm1" -$webClient.DownloadFile($GitHubHelperUrl, $GitHubHelperPath) -$ALGoHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -Write-Host "Downloading AL-Go Helper script from $ALGoHelperUrl" -$ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" -$webClient.DownloadFile($ALGoHelperUrl, $ALGoHelperPath) +$tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" +New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Packages.json' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath . $ALGoHelperPath -local diff --git a/Templates/AppSource App/.AL-Go/localDevEnv.ps1 b/Templates/AppSource App/.AL-Go/localDevEnv.ps1 index af510e228..d744376f3 100644 --- a/Templates/AppSource App/.AL-Go/localDevEnv.ps1 +++ b/Templates/AppSource App/.AL-Go/localDevEnv.ps1 @@ -16,6 +16,21 @@ Param( $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 +function DownloadHelperFile { + param( + [string] $url, + [string] $folder + ) + + $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' + $name = [System.IO.Path]::GetFileName($url) + Write-Host "Downloading $name from $url" + $path = Join-Path $folder $name + Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path + $ProgressPreference = $prevProgressPreference + return $path +} + try { Clear-Host Write-Host @@ -29,17 +44,11 @@ Write-Host -ForegroundColor Yellow @' '@ -$webClient = New-Object System.Net.WebClient -$webClient.CachePolicy = New-Object System.Net.Cache.RequestCachePolicy -argumentList ([System.Net.Cache.RequestCacheLevel]::NoCacheNoStore) -$webClient.Encoding = [System.Text.Encoding]::UTF8 -$GitHubHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -Write-Host "Downloading GitHub Helper module from $GitHubHelperUrl" -$GitHubHelperPath = "$([System.IO.Path]::GetTempFileName()).psm1" -$webClient.DownloadFile($GitHubHelperUrl, $GitHubHelperPath) -$ALGoHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -Write-Host "Downloading AL-Go Helper script from $ALGoHelperUrl" -$ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" -$webClient.DownloadFile($ALGoHelperUrl, $ALGoHelperPath) +$tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" +New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Packages.json' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath . $ALGoHelperPath -local diff --git a/Templates/AppSource App/.github/workflows/AddExistingAppOrTestApp.yaml b/Templates/AppSource App/.github/workflows/AddExistingAppOrTestApp.yaml index 92f55c4d3..bd7bf7f96 100644 --- a/Templates/AppSource App/.github/workflows/AddExistingAppOrTestApp.yaml +++ b/Templates/AppSource App/.github/workflows/AddExistingAppOrTestApp.yaml @@ -24,6 +24,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/CICD.yaml b/Templates/AppSource App/.github/workflows/CICD.yaml index efbe5c81e..bc48f038e 100644 --- a/Templates/AppSource App/.github/workflows/CICD.yaml +++ b/Templates/AppSource App/.github/workflows/CICD.yaml @@ -17,6 +17,7 @@ permissions: contents: read actions: read pages: read + id-token: write env: workflowDepth: 1 @@ -252,7 +253,7 @@ jobs: with: shell: ${{ matrix.shell }} gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext,${{ steps.envName.outputs.envName }}-EnvironmentName,${{ steps.envName.outputs.envName }}_EnvironmentName,EnvironmentName,projects' + getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' - name: Deploy to Business Central id: Deploy diff --git a/Templates/AppSource App/.github/workflows/CreateApp.yaml b/Templates/AppSource App/.github/workflows/CreateApp.yaml index f1117377a..027750ce4 100644 --- a/Templates/AppSource App/.github/workflows/CreateApp.yaml +++ b/Templates/AppSource App/.github/workflows/CreateApp.yaml @@ -34,6 +34,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml index 45cd0f1c4..1bf9f54eb 100644 --- a/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml +++ b/Templates/AppSource App/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml @@ -28,6 +28,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/CreatePerformanceTestApp.yaml b/Templates/AppSource App/.github/workflows/CreatePerformanceTestApp.yaml index 7ee713fc4..6ba04fd09 100644 --- a/Templates/AppSource App/.github/workflows/CreatePerformanceTestApp.yaml +++ b/Templates/AppSource App/.github/workflows/CreatePerformanceTestApp.yaml @@ -40,6 +40,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/CreateRelease.yaml b/Templates/AppSource App/.github/workflows/CreateRelease.yaml index aa1b11157..dd0867fc2 100644 --- a/Templates/AppSource App/.github/workflows/CreateRelease.yaml +++ b/Templates/AppSource App/.github/workflows/CreateRelease.yaml @@ -49,6 +49,7 @@ permissions: contents: write pull-requests: write actions: read + id-token: write concurrency: release diff --git a/Templates/AppSource App/.github/workflows/CreateTestApp.yaml b/Templates/AppSource App/.github/workflows/CreateTestApp.yaml index d4c846834..f1cac123d 100644 --- a/Templates/AppSource App/.github/workflows/CreateTestApp.yaml +++ b/Templates/AppSource App/.github/workflows/CreateTestApp.yaml @@ -36,6 +36,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/Current.yaml b/Templates/AppSource App/.github/workflows/Current.yaml index 1ee652ad6..b0f5fe519 100644 --- a/Templates/AppSource App/.github/workflows/Current.yaml +++ b/Templates/AppSource App/.github/workflows/Current.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/IncrementVersionNumber.yaml b/Templates/AppSource App/.github/workflows/IncrementVersionNumber.yaml index 67b3fcc35..d8b2470c5 100644 --- a/Templates/AppSource App/.github/workflows/IncrementVersionNumber.yaml +++ b/Templates/AppSource App/.github/workflows/IncrementVersionNumber.yaml @@ -24,6 +24,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/NextMajor.yaml b/Templates/AppSource App/.github/workflows/NextMajor.yaml index cea6df96d..14e804b99 100644 --- a/Templates/AppSource App/.github/workflows/NextMajor.yaml +++ b/Templates/AppSource App/.github/workflows/NextMajor.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/NextMinor.yaml b/Templates/AppSource App/.github/workflows/NextMinor.yaml index 86af429af..f816bcec3 100644 --- a/Templates/AppSource App/.github/workflows/NextMinor.yaml +++ b/Templates/AppSource App/.github/workflows/NextMinor.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/PublishToAppSource.yaml b/Templates/AppSource App/.github/workflows/PublishToAppSource.yaml index 67ece3d28..3534528ae 100644 --- a/Templates/AppSource App/.github/workflows/PublishToAppSource.yaml +++ b/Templates/AppSource App/.github/workflows/PublishToAppSource.yaml @@ -20,6 +20,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml index 06f4e3e15..264a06520 100644 --- a/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/AppSource App/.github/workflows/PublishToEnvironment.yaml @@ -14,6 +14,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: @@ -152,7 +153,7 @@ jobs: with: shell: ${{ matrix.shell }} gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext,${{ steps.envName.outputs.envName }}-EnvironmentName,${{ steps.envName.outputs.envName }}_EnvironmentName,EnvironmentName,projects' + getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' - name: Get Artifacts for deployment uses: microsoft/AL-Go-Actions/GetArtifactsForDeployment@main diff --git a/Templates/AppSource App/.github/workflows/PullRequestHandler.yaml b/Templates/AppSource App/.github/workflows/PullRequestHandler.yaml index 56ab98022..39e58308d 100644 --- a/Templates/AppSource App/.github/workflows/PullRequestHandler.yaml +++ b/Templates/AppSource App/.github/workflows/PullRequestHandler.yaml @@ -16,6 +16,7 @@ permissions: contents: read actions: read pull-requests: read + id-token: write env: workflowDepth: 1 diff --git a/Templates/AppSource App/.github/workflows/UpdateGitHubGoSystemFiles.yaml b/Templates/AppSource App/.github/workflows/UpdateGitHubGoSystemFiles.yaml index 0e35b03bc..1b2314e03 100644 --- a/Templates/AppSource App/.github/workflows/UpdateGitHubGoSystemFiles.yaml +++ b/Templates/AppSource App/.github/workflows/UpdateGitHubGoSystemFiles.yaml @@ -18,6 +18,7 @@ on: permissions: contents: read + id-token: write defaults: run: diff --git a/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml b/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml index 64cd99e7b..f52d49741 100644 --- a/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml +++ b/Templates/AppSource App/.github/workflows/_BuildALGoProject.yaml @@ -75,6 +75,7 @@ on: permissions: contents: read actions: read + id-token: write env: ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} diff --git a/Templates/Per Tenant Extension/.AL-Go/cloudDevEnv.ps1 b/Templates/Per Tenant Extension/.AL-Go/cloudDevEnv.ps1 index eca0288fd..750a65cae 100644 --- a/Templates/Per Tenant Extension/.AL-Go/cloudDevEnv.ps1 +++ b/Templates/Per Tenant Extension/.AL-Go/cloudDevEnv.ps1 @@ -12,6 +12,21 @@ Param( $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 +function DownloadHelperFile { + param( + [string] $url, + [string] $folder + ) + + $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' + $name = [System.IO.Path]::GetFileName($url) + Write-Host "Downloading $name from $url" + $path = Join-Path $folder $name + Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path + $ProgressPreference = $prevProgressPreference + return $path +} + try { Clear-Host Write-Host @@ -25,17 +40,11 @@ Write-Host -ForegroundColor Yellow @' '@ -$webClient = New-Object System.Net.WebClient -$webClient.CachePolicy = New-Object System.Net.Cache.RequestCachePolicy -argumentList ([System.Net.Cache.RequestCacheLevel]::NoCacheNoStore) -$webClient.Encoding = [System.Text.Encoding]::UTF8 -$GitHubHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -Write-Host "Downloading GitHub Helper module from $GitHubHelperUrl" -$GitHubHelperPath = "$([System.IO.Path]::GetTempFileName()).psm1" -$webClient.DownloadFile($GitHubHelperUrl, $GitHubHelperPath) -$ALGoHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -Write-Host "Downloading AL-Go Helper script from $ALGoHelperUrl" -$ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" -$webClient.DownloadFile($ALGoHelperUrl, $ALGoHelperPath) +$tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" +New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Packages.json' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath . $ALGoHelperPath -local diff --git a/Templates/Per Tenant Extension/.AL-Go/localDevEnv.ps1 b/Templates/Per Tenant Extension/.AL-Go/localDevEnv.ps1 index af510e228..d744376f3 100644 --- a/Templates/Per Tenant Extension/.AL-Go/localDevEnv.ps1 +++ b/Templates/Per Tenant Extension/.AL-Go/localDevEnv.ps1 @@ -16,6 +16,21 @@ Param( $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 +function DownloadHelperFile { + param( + [string] $url, + [string] $folder + ) + + $prevProgressPreference = $ProgressPreference; $ProgressPreference = 'SilentlyContinue' + $name = [System.IO.Path]::GetFileName($url) + Write-Host "Downloading $name from $url" + $path = Join-Path $folder $name + Invoke-WebRequest -UseBasicParsing -uri $url -OutFile $path + $ProgressPreference = $prevProgressPreference + return $path +} + try { Clear-Host Write-Host @@ -29,17 +44,11 @@ Write-Host -ForegroundColor Yellow @' '@ -$webClient = New-Object System.Net.WebClient -$webClient.CachePolicy = New-Object System.Net.Cache.RequestCachePolicy -argumentList ([System.Net.Cache.RequestCacheLevel]::NoCacheNoStore) -$webClient.Encoding = [System.Text.Encoding]::UTF8 -$GitHubHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -Write-Host "Downloading GitHub Helper module from $GitHubHelperUrl" -$GitHubHelperPath = "$([System.IO.Path]::GetTempFileName()).psm1" -$webClient.DownloadFile($GitHubHelperUrl, $GitHubHelperPath) -$ALGoHelperUrl = 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -Write-Host "Downloading AL-Go Helper script from $ALGoHelperUrl" -$ALGoHelperPath = "$([System.IO.Path]::GetTempFileName()).ps1" -$webClient.DownloadFile($ALGoHelperUrl, $ALGoHelperPath) +$tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" +New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Github-Helper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/main/Packages.json' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath . $ALGoHelperPath -local diff --git a/Templates/Per Tenant Extension/.github/workflows/AddExistingAppOrTestApp.yaml b/Templates/Per Tenant Extension/.github/workflows/AddExistingAppOrTestApp.yaml index 92f55c4d3..bd7bf7f96 100644 --- a/Templates/Per Tenant Extension/.github/workflows/AddExistingAppOrTestApp.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/AddExistingAppOrTestApp.yaml @@ -24,6 +24,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/CICD.yaml b/Templates/Per Tenant Extension/.github/workflows/CICD.yaml index 058b6f365..d76ad4821 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CICD.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CICD.yaml @@ -17,6 +17,7 @@ permissions: contents: read actions: read pages: read + id-token: write env: workflowDepth: 1 @@ -266,7 +267,7 @@ jobs: with: shell: ${{ matrix.shell }} gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext,${{ steps.envName.outputs.envName }}-EnvironmentName,${{ steps.envName.outputs.envName }}_EnvironmentName,EnvironmentName,projects' + getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' - name: Deploy to Business Central id: Deploy diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateApp.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateApp.yaml index f1117377a..027750ce4 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreateApp.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreateApp.yaml @@ -34,6 +34,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml index 45cd0f1c4..1bf9f54eb 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreateOnlineDevelopmentEnvironment.yaml @@ -28,6 +28,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/CreatePerformanceTestApp.yaml b/Templates/Per Tenant Extension/.github/workflows/CreatePerformanceTestApp.yaml index 7ee713fc4..6ba04fd09 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreatePerformanceTestApp.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreatePerformanceTestApp.yaml @@ -40,6 +40,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateRelease.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateRelease.yaml index 45489fbf8..e10275b33 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreateRelease.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreateRelease.yaml @@ -49,6 +49,7 @@ permissions: contents: write pull-requests: write actions: read + id-token: write concurrency: release diff --git a/Templates/Per Tenant Extension/.github/workflows/CreateTestApp.yaml b/Templates/Per Tenant Extension/.github/workflows/CreateTestApp.yaml index d4c846834..f1cac123d 100644 --- a/Templates/Per Tenant Extension/.github/workflows/CreateTestApp.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/CreateTestApp.yaml @@ -36,6 +36,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/Current.yaml b/Templates/Per Tenant Extension/.github/workflows/Current.yaml index 1ee652ad6..b0f5fe519 100644 --- a/Templates/Per Tenant Extension/.github/workflows/Current.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/Current.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/IncrementVersionNumber.yaml b/Templates/Per Tenant Extension/.github/workflows/IncrementVersionNumber.yaml index 67b3fcc35..d8b2470c5 100644 --- a/Templates/Per Tenant Extension/.github/workflows/IncrementVersionNumber.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/IncrementVersionNumber.yaml @@ -24,6 +24,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/NextMajor.yaml b/Templates/Per Tenant Extension/.github/workflows/NextMajor.yaml index cea6df96d..14e804b99 100644 --- a/Templates/Per Tenant Extension/.github/workflows/NextMajor.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/NextMajor.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/NextMinor.yaml b/Templates/Per Tenant Extension/.github/workflows/NextMinor.yaml index 86af429af..f816bcec3 100644 --- a/Templates/Per Tenant Extension/.github/workflows/NextMinor.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/NextMinor.yaml @@ -6,6 +6,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml index 06f4e3e15..264a06520 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PublishToEnvironment.yaml @@ -14,6 +14,7 @@ on: permissions: contents: read actions: read + id-token: write defaults: run: @@ -152,7 +153,7 @@ jobs: with: shell: ${{ matrix.shell }} gitHubSecrets: ${{ toJson(secrets) }} - getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext,${{ steps.envName.outputs.envName }}-EnvironmentName,${{ steps.envName.outputs.envName }}_EnvironmentName,EnvironmentName,projects' + getSecrets: '${{ steps.envName.outputs.envName }}-AuthContext,${{ steps.envName.outputs.envName }}_AuthContext,AuthContext' - name: Get Artifacts for deployment uses: microsoft/AL-Go-Actions/GetArtifactsForDeployment@main diff --git a/Templates/Per Tenant Extension/.github/workflows/PullPowerPlatformChanges.yaml b/Templates/Per Tenant Extension/.github/workflows/PullPowerPlatformChanges.yaml index e0c152459..2ac1168f9 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PullPowerPlatformChanges.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PullPowerPlatformChanges.yaml @@ -21,6 +21,7 @@ on: permissions: contents: write pull-requests: write + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/PullRequestHandler.yaml b/Templates/Per Tenant Extension/.github/workflows/PullRequestHandler.yaml index 56ab98022..39e58308d 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PullRequestHandler.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PullRequestHandler.yaml @@ -16,6 +16,7 @@ permissions: contents: read actions: read pull-requests: read + id-token: write env: workflowDepth: 1 diff --git a/Templates/Per Tenant Extension/.github/workflows/PushPowerPlatformChanges.yaml b/Templates/Per Tenant Extension/.github/workflows/PushPowerPlatformChanges.yaml index ac4492287..fb102b84d 100644 --- a/Templates/Per Tenant Extension/.github/workflows/PushPowerPlatformChanges.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/PushPowerPlatformChanges.yaml @@ -12,6 +12,7 @@ on: permissions: contents: read + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/UpdateGitHubGoSystemFiles.yaml b/Templates/Per Tenant Extension/.github/workflows/UpdateGitHubGoSystemFiles.yaml index 0e35b03bc..1b2314e03 100644 --- a/Templates/Per Tenant Extension/.github/workflows/UpdateGitHubGoSystemFiles.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/UpdateGitHubGoSystemFiles.yaml @@ -18,6 +18,7 @@ on: permissions: contents: read + id-token: write defaults: run: diff --git a/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml b/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml index 64cd99e7b..f52d49741 100644 --- a/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml +++ b/Templates/Per Tenant Extension/.github/workflows/_BuildALGoProject.yaml @@ -75,6 +75,7 @@ on: permissions: contents: read actions: read + id-token: write env: ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} diff --git a/e2eTests/scenarios/FederatedCredentials/runtest.ps1 b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 new file mode 100644 index 000000000..d7ef99ec5 --- /dev/null +++ b/e2eTests/scenarios/FederatedCredentials/runtest.ps1 @@ -0,0 +1,102 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidGlobalVars', '', Justification = 'Global vars used for local test execution only.')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'All scenario tests have equal parameter set.')] +Param( + [switch] $github, + [string] $githubOwner = $global:E2EgithubOwner, + [string] $repoName = [System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetTempFileName()), + [string] $token = ($Global:SecureE2EPAT | Get-PlainText), + [string] $pteTemplate = $global:pteTemplate, + [string] $appSourceTemplate = $global:appSourceTemplate, + [string] $adminCenterApiToken = ($global:SecureAdminCenterApiToken | Get-PlainText) +) + +Write-Host -ForegroundColor Yellow @' +# ______ _ _ _ _____ _ _ _ _ +# | ____| | | | | | | / ____| | | | | (_) | | +# | |__ ___ __| | ___ _ __ __ _| |_ ___ __| | | | _ __ ___ __| | ___ _ __ | |_ _ __ _| |___ +# | __/ _ \/ _` |/ _ \ '__/ _` | __/ _ \/ _` | | | | '__/ _ \/ _` |/ _ \ '_ \| __| |/ _` | / __| +# | | | __/ (_| | __/ | | (_| | || __/ (_| | | |____| | | __/ (_| | __/ | | | |_| | (_| | \__ \ +# |_| \___|\__,_|\___|_| \__,_|\__\___|\__,_| \_____|_| \___|\__,_|\___|_| |_|\__|_|\__,_|_|___/ +# +# +# +# This test uses the bcsamples-bingmaps.appsource repository and will deliver a new build of the app to AppSource. +# The bcsamples-bingmaps.appsource repository is setup to use an Azure KeyVault for secrets and app signing. +# The bcSamples-bingmaps.appsource repository is setup for continuous delivery to AppSource +# This test will deliver another build of the latest app version already delivered to AppSource (without go-live) +# +# This test tests the following scenario: +# +# bcsamples-bingmaps.appsource is setup to use an Azure KeyVault for secrets and app signing +# Access to the Azure KeyVault is using federated credentials (branches main and e2e) +# bcsamples-bingmaps.appsource is using a Entra ID app registration for delivering to AppSource +# The Entra ID app registration is using federated credentials (branches main and e2e) +# +# - Remove branch e2e in repository microsoft/bcsamples-bingmaps.appsource (if it exists) +# - Create a new branch called e2e in repository microsoft/bcsamples-bingmaps.appsource (based on main) +# - Update AL-Go System Files in branch e2e in repository microsoft/bcsamples-bingmaps.appsource +# - Invoke CI/CD in branch e2e in repository microsoft/bcsamples-bingmaps.appsource +# - Check that artifacts are created and signed +# - Check that the app is delivered to AppSource +# - Remove the branch e2e in repository microsoft/bcsamples-bingmaps.appsource +# +'@ + +$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 + +Remove-Module e2eTestHelper -ErrorAction SilentlyContinue +Import-Module (Join-Path $PSScriptRoot "..\..\e2eTestHelper.psm1") -DisableNameChecking + +$branch = "e2e" +$template = "https://github.com/$appSourceTemplate" +$repository = 'microsoft/bcsamples-bingmaps.appsource' + +SetTokenAndRepository -github:$github -githubOwner $githubOwner -token $token -repository $repository +$headers = @{ + "Authorization" = "token $token" + "X-GitHub-Api-Version" = "2022-11-28" + "Accept" = "application/vnd.github+json" +} +$existingBranch = gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/$repository/branches/$branch 2> $null | ConvertFrom-Json +if ($existingBranch.PSObject.Properties.Name -eq 'Name' -and $existingBranch.Name -eq $branch) { + Write-Host "Removing existing branch $branch" + Invoke-RestMethod -Method Delete -Uri "https://api.github.com/repos/$repository/git/refs/heads/$branch" -Headers $headers + Start-Sleep -Seconds 10 +} +$latestSha = (gh api /repos/$repository/commits/main | ConvertFrom-Json).sha +gh api --method POST -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/$repository/git/refs -f ref=refs/heads/$branch -f sha=$latestSha + +# Upgrade AL-Go System Files to test version +RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $template -repository $repository -branch $branch | Out-Null + +# Run CI/CD workflow +$run = RunCICD -repository $repository -branch $branch -wait + +# Check that workflow run uses federated credentials and signing was successful +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'Build Main App (Default) Main App (Default)' -stepName 'Sign' -expectedText 'Connecting to Azure using clientId and federated token' +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'Build Main App (Default) Main App (Default)' -stepName 'Sign' -expectedText 'Signing succeeded' + +# Check that Deliver to AppSource uses federated credentials and that a new submission was created +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'Deliver to AppSource' -stepName 'Read secrets' -expectedText 'Query federated token' +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'Deliver to AppSource' -stepName 'Deliver' -expectedText 'New AppSource submission created' + +# Test artifacts generated +$artifacts = gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/$repository/actions/runs/$($run.id)/artifacts | ConvertFrom-Json +@($artifacts.artifacts.Name) -like "Library Apps-$branch-Apps-*.*.*.0" | Should -Be $true +@($artifacts.artifacts.Name) -like "Main App-$branch-Apps-*.*.*.0" | Should -Be $true +@($artifacts.artifacts.Name) -like "Main App-$branch-Dependencies-*.*.*.0" | Should -Be $true + +Write-Host "Download build artifacts" +invoke-gh run download $run.id --repo $repository --dir 'signedApps' + +$noOfApps = 0 +Get-Item "signedApps/Main App-$branch-Apps-*.*.*.0/*.app" | ForEach-Object { + $appFile = $_.FullName + Write-Host "Check that $appFile was signed" + [System.Text.Encoding]::Ascii.GetString([System.IO.File]::ReadAllBytes($appFile)).indexof('DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA') | Should -BeGreaterThan -1 + $noOfApps++ +} +# Check that two apps were signed +$noOfApps | Should -Be 2 + +Invoke-RestMethod -Method Delete -Uri "https://api.github.com/repos/$repository/git/refs/heads/$branch" -Headers $headers