Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support authenticating as GitHub Apps instead of using Personal Access Tokens #1311

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a091a35
Support GitHub App Auth
freddydk Nov 17, 2024
9463f40
catch
freddydk Nov 17, 2024
dd0cbea
10
freddydk Nov 17, 2024
7c7253d
dumps
freddydk Nov 17, 2024
32aab8d
mask multi
freddydk Nov 17, 2024
0c191ab
add message
freddydk Nov 17, 2024
df1c6e6
precommit
freddydk Nov 17, 2024
ff16e55
token exchange
freddydk Nov 18, 2024
add7df9
getreal
freddydk Nov 18, 2024
38b7860
tests
freddydk Nov 18, 2024
af60cca
getreal
freddydk Nov 18, 2024
6f09716
x
freddydk Nov 18, 2024
e3e4968
use real
freddydk Nov 18, 2024
0f9ae18
use real
freddydk Nov 18, 2024
d0157f6
no user
freddydk Nov 18, 2024
f358ab8
use githubOwner
freddydk Nov 18, 2024
936c912
dump
freddydk Nov 18, 2024
945ca71
use repo
freddydk Nov 18, 2024
dae2cdc
no config
freddydk Nov 18, 2024
cd8b221
use repo
freddydk Nov 18, 2024
1524490
fix token
freddydk Nov 18, 2024
28232d6
mask value
freddydk Nov 18, 2024
1402a6f
move
freddydk Nov 18, 2024
9491ba7
use gh
freddydk Nov 18, 2024
9c30d28
remove silent
freddydk Nov 18, 2024
8b10b20
remove line breaks
freddydk Nov 19, 2024
3f1c796
Merge branch 'main' into noPAT
freddydk Nov 19, 2024
e9ee5e8
fix e2e
freddydk Nov 19, 2024
2501a3b
Merge branch 'noPAT' of https://github.com/freddydk/AL-Go into noPAT
freddydk Nov 19, 2024
112ebc3
silent
freddydk Nov 19, 2024
85014aa
use gfh
freddydk Nov 19, 2024
cea8eda
jit
freddydk Nov 19, 2024
9ba3af2
use real token
freddydk Nov 19, 2024
cdd459c
get token for repo
freddydk Nov 19, 2024
d7d440b
inc count
freddydk Nov 19, 2024
cd03ea6
token cache
freddydk Nov 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Actions/AL-Go-Helper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $defaultCICDPushBranches = @( 'main', 'release/*', 'feature/*' )
$defaultCICDPullRequestBranches = @( 'main' )
$runningLocal = $local.IsPresent
$defaultBcContainerHelperVersion = "preview" # Must be double quotes. Will be replaced by BcContainerHelperVersion if necessary in the deploy step - ex. "https://github.com/organization/navcontainerhelper/archive/refs/heads/branch.zip"
$notSecretProperties = @("Scopes","TenantId","BlobName","ContainerName","StorageAccountName","ServerUrl","ppUserName")
$notSecretProperties = @("Scopes","TenantId","BlobName","ContainerName","StorageAccountName","ServerUrl","ppUserName","GitHubAppClientId")

$runAlPipelineOverrides = @(
"DockerPull"
Expand Down Expand Up @@ -1315,12 +1315,13 @@ function CloneIntoNewFolder {
$baseFolder = Join-Path ([System.IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString())
New-Item $baseFolder -ItemType Directory | Out-Null
Set-Location $baseFolder
$serverUri = [Uri]::new($env:GITHUB_SERVER_URL)
$serverUrl = "$($serverUri.Scheme)://$($actor):$($token)@$($serverUri.Host)/$($env:GITHUB_REPOSITORY)"

# Environment variables for hub commands
$env:GITHUB_USER = $actor
$env:GITHUB_TOKEN = $token
$env:GITHUB_TOKEN = GetRealToken -token $token

$serverUri = [Uri]::new($env:GITHUB_SERVER_URL)
$serverUrl = "$($serverUri.Scheme)://$($env:GITHUB_USER):$($env:GITHUB_TOKEN)@$($serverUri.Host)/$($env:GITHUB_REPOSITORY)"

# Configure git
invoke-git config --global user.email "[email protected]"
Expand Down
13 changes: 10 additions & 3 deletions Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,22 @@ function GetLatestTemplateSha {

try {
$response = InvokeWebRequest -Headers $headers -Uri "$apiUrl/branches?per_page=100" -retry
$branchInfo = ($response.content | ConvertFrom-Json) | Where-Object { $_.Name -eq $branch }
} catch {
if ($_.Exception.Message -like "*401*") {
throw "Failed to update AL-Go System Files. Make sure that the personal access token, defined in the secret called GhTokenWorkflow, is not expired and it has permission to update workflows. (Error was $($_.Exception.Message))"
} else {
try {
$headers.Remove('Authorization')
$response = InvokeWebRequest -Headers $headers -Uri "$apiUrl/branches?per_page=100" -retry
}
catch {
throw "Failed to update AL-Go System Files. Make sure that the personal access token, defined in the secret called GhTokenWorkflow, is not expired and it has permission to update workflows. (Error was $($_.Exception.Message))"
}
}
else {
throw $_.Exception.Message
}
}

$branchInfo = ($response.content | ConvertFrom-Json) | Where-Object { $_.Name -eq $branch }
if (!$branchInfo) {
throw "$templateUrl doesn't exist"
}
Expand Down
7 changes: 2 additions & 5 deletions Actions/CheckForUpdates/CheckForUpdates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,12 @@ if ($update -eq 'Y') {
throw "A personal access token with permissions to modify Workflows is needed. You must add a secret called GhTokenWorkflow containing a personal access token. You can Generate a new token from https://github.com/settings/tokens. Make sure that the workflow scope is checked."
}
else {
$token = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($token))
$token = GetRealToken -token ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($token)))
}
}

# Use Authenticated API request to avoid the 60 API calls per hour limit
$headers = @{
"Accept" = "application/vnd.github.baptiste-preview+json"
"Authorization" = "Bearer $token"
}
$headers = GetHeaders -token $token

if (-not $templateUrl.Contains('@')) {
$templateUrl += "@main"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
)

function IsGitHubPagesAvailable() {
$headers = GetHeader -token $env:GITHUB_TOKEN
$headers = GetHeaders -token $env:GITHUB_TOKEN
$url = "$($ENV:GITHUB_API_URL)/repos/$($ENV:GITHUB_REPOSITORY)/pages"
try {
Write-Host "Requesting GitHub Pages settings from GitHub"
Expand All @@ -20,7 +20,7 @@ function IsGitHubPagesAvailable() {
}

function GetGitHubEnvironments() {
$headers = GetHeader -token $env:GITHUB_TOKEN
$headers = GetHeaders -token $env:GITHUB_TOKEN
$url = "$($ENV:GITHUB_API_URL)/repos/$($ENV:GITHUB_REPOSITORY)/environments"
try {
Write-Host "Requesting environments from GitHub"
Expand All @@ -36,7 +36,7 @@ function GetGitHubEnvironments() {
function Get-BranchesFromPolicy($ghEnvironment) {
if ($ghEnvironment) {
# Environment is defined in GitHub - check protection rules
$headers = GetHeader -token $env:GITHUB_TOKEN
$headers = GetHeaders -token $env:GITHUB_TOKEN
$branchPolicy = ($ghEnvironment.protection_rules | Where-Object { $_.type -eq "branch_policy" })
if ($branchPolicy) {
if ($ghEnvironment.deployment_branch_policy.protected_branches) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ function Get-ModifiedFiles {

$url = "$($env:GITHUB_API_URL)/repos/$($env:GITHUB_REPOSITORY)/compare/$($ghEvent.pull_request.base.sha)...$($ghEvent.pull_request.head.sha)"

$headers = @{
"Authorization" = "token $token"
"Accept" = "application/vnd.github.baptiste-preview+json"
}
$headers = GetHeaders -token $token

$response = (InvokeWebRequest -Headers $headers -Uri $url).Content | ConvertFrom-Json

Expand Down
157 changes: 126 additions & 31 deletions Actions/Github-Helper.psm1
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
$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','{','|','}','~')
$script:realTokenCache = @{
"token" = ''
"repository" = ''
"realToken" = ''
"expires" = [datetime]::Now
}

function MaskValue {
Param(
[string] $key,
[string] $value
)

Write-Host "Masking value for $key"
$value.Split("`n") | ForEach-Object {
Write-Host "::add-mask::$_"
}

$val2 = ""
$value.ToCharArray() | ForEach-Object {
$chint = [int]$_
if ($chint -lt 32 -or $chint -gt 126 ) {
$val2 += $_
}
else {
$val2 += $script:escchars[$chint-32]
}
}

if ($val2 -ne $value) {
$val2.Split("`n") | ForEach-Object {
Write-Host "::add-mask::$_"
}
}
Write-Host "::add-mask::$([Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($value)))"
}

function GetExtendedErrorMessage {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification="We want to ignore errors")]
Param(
Expand Down Expand Up @@ -501,7 +539,7 @@ function GetReleases {
)

Write-Host "Analyzing releases $api_url/repos/$repository/releases"
$releases = (InvokeWebRequest -Headers (GetHeader -token $token) -Uri "$api_url/repos/$repository/releases").Content | ConvertFrom-Json
$releases = (InvokeWebRequest -Headers (GetHeaders -token $token) -Uri "$api_url/repos/$repository/releases").Content | ConvertFrom-Json
if ($releases.Count -gt 1) {
# Sort by SemVer tag
try {
Expand Down Expand Up @@ -556,20 +594,70 @@ function GetLatestRelease {
$latestRelease
}

function GetHeader {
function GetRealToken {
Param(
[string] $token,
[string] $api_url = $ENV:GITHUB_API_URL,
[string] $repository = $ENV:GITHUB_REPOSITORY
)

if (!($token.StartsWith("{"))) {
# not a json token
return $token
}
elseif ($script:realTokenCache.token -eq $token -and $script:realTokenCache.repository -eq $repository -and $script:realTokenCache.expires -gt [datetime]::Now.AddMinutes(10)) {
# Same token request and cached token won't expire in 10 minutes
Write-Host "return cached token"
return $script:realTokenCache.realToken
}
else {
try {
$json = $token | ConvertFrom-Json
$gitHubAppClientId = $json.GitHubAppClientId
$privateKey = $json.PrivateKey
Write-Host "Using GitHub App with ClientId $gitHubAppClientId for authentication"
$jwt = GenerateJwtForTokenRequest -gitHubAppClientId $gitHubAppClientId -privateKey $privateKey
$headers = @{
"Accept" = "application/vnd.github+json"
"Authorization" = "Bearer $jwt"
"X-GitHub-Api-Version" = "2022-11-28"
}
Write-Host "Get App Info $api_url/repos/$repository/installation"
$appinfo = Invoke-RestMethod -Method GET -UseBasicParsing -Headers $headers -Uri "$api_url/repos/$repository/installation"
Write-Host "Get Token Response $($appInfo.access_tokens_url)"
$tokenResponse = Invoke-RestMethod -Method POST -UseBasicParsing -Headers $headers -Uri $appInfo.access_tokens_url
Write-Host "return token"
$script:realTokenCache = @{
"token" = $token
"repository" = $repository
"realToken" = $tokenResponse.token
"expires" = [datetime]::Now.AddSeconds($tokenResponse.expires_in)
}
return $tokenResponse.token
}
catch {
# Not a json token
return $token
}
}
}

function GetHeaders {
param (
[string] $token,
[string] $accept = "application/vnd.github+json",
[string] $apiVersion = "2022-11-28"
[string] $apiVersion = "2022-11-28",
[string] $api_url = $ENV:GITHUB_API_URL,
[string] $repository = $ENV:GITHUB_REPOSITORY
)
$headers = @{
"Accept" = $accept
"X-GitHub-Api-Version" = $apiVersion
}
if (![string]::IsNullOrEmpty($token)) {
$headers["Authorization"] = "token $token"
$realToken = GetRealToken -token $token -api_url $api_url -repository $repository
$headers["Authorization"] = "token $realToken"
}

return $headers
}

Expand Down Expand Up @@ -616,7 +704,7 @@ function GetReleaseNotes {
$postParams["target_commitish"] = $target_commitish
}

InvokeWebRequest -Headers (GetHeader -token $token) -Method POST -Body ($postParams | ConvertTo-Json) -Uri "$api_url/repos/$repository/releases/generate-notes"
InvokeWebRequest -Headers (GetHeaders -token $token) -Method POST -Body ($postParams | ConvertTo-Json) -Uri "$api_url/repos/$repository/releases/generate-notes"
}

function DownloadRelease {
Expand All @@ -636,7 +724,7 @@ function DownloadRelease {
if ([string]::IsNullOrEmpty($token)) {
$token = invoke-gh -silent -returnValue auth token
}
$headers = GetHeader -token $token -accept "application/octet-stream"
$headers = GetHeaders -token $token -accept "application/octet-stream"
foreach($project in $projects.Split(',')) {
# GitHub replaces series of special characters with a single dot when uploading release assets
$project = [Uri]::EscapeDataString($project.Replace('\','_').Replace('/','_').Replace(' ','.')).Replace('%2A','*').Replace('%3F','?').Replace('%','')
Expand Down Expand Up @@ -669,25 +757,6 @@ function DownloadRelease {
}
}

function CheckRateLimit {
Param(
[string] $token = ''
)

$headers = GetHeader -token $token
$rate = (InvokeWebRequest -Headers $headers -Uri "https://api.github.com/rate_limit").Content | ConvertFrom-Json
$rate | ConvertTo-Json -Depth 99 | Out-Host
$rate = $rate.rate
$percent = [int]($rate.remaining*100/$rate.limit)
Write-Host "$($rate.remaining) API calls remaining out of $($rate.limit) ($percent%)"
if ($percent -lt 10) {
$resetTimeStamp = ([datetime] '1970-01-01Z').AddSeconds($rate.reset)
$waitTime = $resetTimeStamp.Subtract([datetime]::Now)
Write-Host "Less than 10% API calls left, waiting for $($waitTime.TotalSeconds) seconds for limits to reset."
Start-Sleep -seconds ($waitTime.TotalSeconds+1)
}
}

# Get Content of UTF8 encoded file as a string with LF line endings
# No empty line at the end of the file
function Get-ContentLF {
Expand Down Expand Up @@ -771,7 +840,7 @@ function CheckBuildJobsInWorkflowRun {
[string] $workflowRunId
)

$headers = GetHeader -token $token
$headers = GetHeaders -token $token
$per_page = 100
$page = 1

Expand Down Expand Up @@ -822,7 +891,7 @@ function FindLatestSuccessfulCICDRun {
[string] $token
)

$headers = GetHeader -token $token
$headers = GetHeaders -token $token
$lastSuccessfulCICDRun = 0
$per_page = 100
$page = 1
Expand Down Expand Up @@ -899,7 +968,7 @@ function GetArtifactsFromWorkflowRun {

Write-Host "Getting artifacts for workflow run $workflowRun, mask $mask, projects $projects and version $version"

$headers = GetHeader -token $token
$headers = GetHeaders -token $token

$foundArtifacts = @()
$per_page = 100
Expand Down Expand Up @@ -975,7 +1044,7 @@ function GetArtifacts {
)

$refname = $branch.Replace('/','_')
$headers = GetHeader -token $token
$headers = GetHeaders -token $token
if ($version -eq 'latest') { $version = '*' }

# For latest version, use the artifacts from the last successful CICD run
Expand Down Expand Up @@ -1082,7 +1151,7 @@ function DownloadArtifact {
if ([string]::IsNullOrEmpty($token)) {
$token = invoke-gh -silent -returnValue auth token
}
$headers = GetHeader -token $token
$headers = GetHeaders -token $token
$foldername = Join-Path $path $artifact.Name
$filename = "$foldername.zip"
InvokeWebRequest -Headers $headers -Uri $artifact.archive_download_url -OutFile $filename
Expand All @@ -1098,3 +1167,29 @@ function DownloadArtifact {
return $filename
}
}

function GenerateJwtForTokenRequest {
Param(
[string] $gitHubAppClientId,
[string] $privateKey
)

$header = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
alg = "RS256"
typ = "JWT"
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');

$payload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes((ConvertTo-Json -InputObject @{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there a library to use instead of these calculations?

If we are to do our own implementation, let's create a separate PS module for JWT. Something that could be tested preferably.

iat = [System.DateTimeOffset]::UtcNow.AddSeconds(-10).ToUnixTimeSeconds()
exp = [System.DateTimeOffset]::UtcNow.AddMinutes(10).ToUnixTimeSeconds()
iss = $gitHubAppClientId
}))).TrimEnd('=').Replace('+', '-').Replace('/', '_');
$signature = pwsh -command {
$rsa = [System.Security.Cryptography.RSA]::Create()
$privateKey = "$($args[1])"
$rsa.ImportFromPem($privateKey)
$signature = [Convert]::ToBase64String($rsa.SignData([System.Text.Encoding]::UTF8.GetBytes($args[0]), [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1)).TrimEnd('=').Replace('+', '-').Replace('/', '_')
Write-OutPut $signature
} -args "$header.$payload", $privateKey
return "$header.$payload.$signature"
}
2 changes: 1 addition & 1 deletion Actions/PullPowerPlatformChanges/GitCommitChanges.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Set-Location -Path $location

# Environment variables for hub commands
$env:GITHUB_USER = $actor
$env:GITHUB_TOKEN = $token
$env:GITHUB_TOKEN = GetRealToken -token $token

# Commit from the new folder
Write-Host "Committing changes from the new folder $Location\$PowerPlatformSolutionName to branch $gitHubBranch"
Expand Down
Loading
Loading