From 653f4b13ff9a89f9936e41f0f39711083844fa66 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 5 Dec 2024 21:20:42 +0100 Subject: [PATCH 01/29] Simplify the token refresh tech for UAT --- src/functions/public/API/Invoke-GitHubAPI.ps1 | 6 +----- .../public/Auth/Update-GitHubUserAccessToken.ps1 | 11 ++++++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index baec6bf7a..50f04c6ac 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -98,11 +98,7 @@ switch ($TokenType) { 'ghu' { if (Test-GitHubAccessTokenRefreshRequired -Context $Context) { - # TODO: Ensure it can pass the context object, and have it update the context object - # TODO: Should it return the new context with a -PassThru parameter? - # $Context = Update-GitHubUserAccessToken -Context $Context - Update-GitHubUserAccessToken -Context $Context - $Token = (Get-GitHubContextSetting -Name 'Token' -Context $Context) + $Token = Update-GitHubUserAccessToken -Context $Context -PassThru } } 'PEM' { diff --git a/src/functions/public/Auth/Update-GitHubUserAccessToken.ps1 b/src/functions/public/Auth/Update-GitHubUserAccessToken.ps1 index 93698afc2..36a71abeb 100644 --- a/src/functions/public/Auth/Update-GitHubUserAccessToken.ps1 +++ b/src/functions/public/Auth/Update-GitHubUserAccessToken.ps1 @@ -19,6 +19,7 @@ .NOTES [Refreshing user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens) #> + [OutputType([securestring])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Long links for documentation.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Is the CLI part of the module.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'The tokens are recieved as clear text. Mitigating exposure by removing variables and performing garbage collection.')] @@ -27,7 +28,11 @@ # The context to run the command in. Used to get the details for the API call. # Can be either a string or a GitHubContext object. [Parameter()] - [object] $Context = (Get-GitHubContext) + [object] $Context = (Get-GitHubContext), + + # Return the new access token. + [Parameter()] + [switch] $PassThru ) $Context = Resolve-GitHubContext -Context $Context @@ -80,4 +85,8 @@ if ($PSCmdlet.ShouldProcess('Access token', 'Update/refresh')) { Set-GitHubContextSetting @settings -Context $Context } + + if ($PassThru) { + $settings['Token'] + } } From 70a0932ea6ec984cf0b36564943c1936b95ef2a9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 5 Dec 2024 23:22:47 +0100 Subject: [PATCH 02/29] Get authed app or a specific app --- .../private/Apps/Get-GitHubAppByName.ps1 | 44 +++++++++++++++++++ .../Apps/Get-GitHubAuthenticatedApp.ps1 | 43 ++++++++++++++++++ .../public/Apps/GitHub Apps/Get-GitHubApp.ps1 | 41 ++++++++++------- 3 files changed, 112 insertions(+), 16 deletions(-) create mode 100644 src/functions/private/Apps/Get-GitHubAppByName.ps1 create mode 100644 src/functions/private/Apps/Get-GitHubAuthenticatedApp.ps1 diff --git a/src/functions/private/Apps/Get-GitHubAppByName.ps1 b/src/functions/private/Apps/Get-GitHubAppByName.ps1 new file mode 100644 index 000000000..83ce43566 --- /dev/null +++ b/src/functions/private/Apps/Get-GitHubAppByName.ps1 @@ -0,0 +1,44 @@ +filter Get-GitHubAppByName { + <# + .SYNOPSIS + Get an app + + .DESCRIPTION + Gets a single GitHub App using the app's slug. + + .EXAMPLE + Get-GitHubAppByName -AppSlug 'github-actions' + + Gets the GitHub App with the slug 'github-actions'. + + .NOTES + [Get an app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-app) + #> + [OutputType([pscustomobject])] + [CmdletBinding()] + param( + # The AppSlug is just the URL-friendly name of a GitHub App. + # You can find this on the settings page for your GitHub App (e.g., https://github.com/settings/apps/). + # Example: 'github-actions' + [Parameter(Mandatory)] + [Alias('Name')] + [string] $AppSlug, + + # The context to run the command in. Used to get the details for the API call. + # Can be either a string or a GitHubContext object. + [Parameter()] + [object] $Context = (Get-GitHubContext) + ) + + $Context = Resolve-GitHubContext -Context $Context + + $inputObject = @{ + Context = $Context + APIEndpoint = "/apps/$AppSlug" + Method = 'GET' + } + + Invoke-GitHubAPI @inputObject | ForEach-Object { + Write-Output $_.Response + } +} diff --git a/src/functions/private/Apps/Get-GitHubAuthenticatedApp.ps1 b/src/functions/private/Apps/Get-GitHubAuthenticatedApp.ps1 new file mode 100644 index 000000000..cf0fb44bd --- /dev/null +++ b/src/functions/private/Apps/Get-GitHubAuthenticatedApp.ps1 @@ -0,0 +1,43 @@ +filter Get-GitHubAuthenticatedApp { + <# + .SYNOPSIS + Get the authenticated app + + .DESCRIPTION + Returns the GitHub App associated with the authentication credentials used. To see how many app installations are associated with this + GitHub App, see the `installations_count` in the response. For more details about your app's installations, see the + "[List installations for the authenticated app](https://docs.github.com/rest/apps/apps#list-installations-for-the-authenticated-app)" + endpoint. + + You must use a [JWT](https://docs.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app) + to access this endpoint. + + .EXAMPLE + Get-GitHubAuthenticatedApp + + Get the authenticated app. + + .NOTES + [Get the authenticated app](https://docs.github.com/rest/apps/apps#get-an-app) + #> + [OutputType([pscustomobject])] + [CmdletBinding()] + param( + # The context to run the command in. Used to get the details for the API call. + # Can be either a string or a GitHubContext object. + [Parameter()] + [object] $Context = (Get-GitHubContext) + ) + + $Context = Resolve-GitHubContext -Context $Context + + $inputObject = @{ + Context = $Context + APIEndpoint = '/app' + Method = 'GET' + } + + Invoke-GitHubAPI @inputObject | ForEach-Object { + Write-Output $_.Response + } +} diff --git a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 index 843995835..598e88a3f 100644 --- a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 +++ b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 @@ -1,28 +1,38 @@ filter Get-GitHubApp { <# .SYNOPSIS - Get the authenticated app + Get the authenticated app or a specific app by its slug. .DESCRIPTION - Returns the GitHub App associated with the authentication credentials used. To see how many app installations are associated with this - GitHub App, see the `installations_count` in the response. For more details about your app's installations, see the - "[List installations for the authenticated app](https://docs.github.com/rest/apps/apps#list-installations-for-the-authenticated-app)" - endpoint. - - You must use a [JWT](https://docs.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-a-github-app) - to access this endpoint. + Returns a GitHub App associated with the authentication credentials used or the provided app-slug. .EXAMPLE Get-GitHubApp Get the authenticated app. + .EXAMPLE + Get-GitHubApp -AppSlug 'github-actions' + + Get the GitHub App with the slug 'github-actions'. + .NOTES + [Get an app](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-app) [Get the authenticated app | GitHub Docs](https://docs.github.com/rest/apps/apps#get-the-authenticated-app) #> [OutputType([pscustomobject])] [CmdletBinding()] param( + # The AppSlug is just the URL-friendly name of a GitHub App. + # You can find this on the settings page for your GitHub App (e.g., https://github.com/settings/apps/). + # Example: 'github-actions' + [Parameter( + Mandatory, + ParameterSetName = 'BySlug' + )] + [Alias('Name')] + [string] $AppSlug, + # The context to run the command in. Used to get the details for the API call. # Can be either a string or a GitHubContext object. [Parameter()] @@ -31,13 +41,12 @@ $Context = Resolve-GitHubContext -Context $Context - $inputObject = @{ - Context = $Context - APIEndpoint = '/app' - Method = 'GET' - } - - Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + switch ($PSCmdlet.ParameterSetName) { + 'BySlug' { + Get-GitHubAppByName -AppSlug $AppSlug -Context $Context + } + default { + Get-GitHubAuthenticatedApp -Context $Context + } } } From 2eafc9cd43608090c0ce7c9d841b40a3e098f45a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 5 Dec 2024 23:32:05 +0100 Subject: [PATCH 03/29] No bare URLs --- src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 index 598e88a3f..ba50c88e6 100644 --- a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 +++ b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 @@ -24,7 +24,7 @@ [CmdletBinding()] param( # The AppSlug is just the URL-friendly name of a GitHub App. - # You can find this on the settings page for your GitHub App (e.g., https://github.com/settings/apps/). + # You can find this on the settings page for your GitHub App (e.g., ). # Example: 'github-actions' [Parameter( Mandatory, From 31e936d47931dd5b121bf0435eed619258824158 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Thu, 5 Dec 2024 23:40:14 +0100 Subject: [PATCH 04/29] Fix --- src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 | 2 +- tools/utilities/GitHubAPI.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 index ba50c88e6..9055ec5ee 100644 --- a/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 +++ b/src/functions/public/Apps/GitHub Apps/Get-GitHubApp.ps1 @@ -21,7 +21,7 @@ [Get the authenticated app | GitHub Docs](https://docs.github.com/rest/apps/apps#get-the-authenticated-app) #> [OutputType([pscustomobject])] - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] param( # The AppSlug is just the URL-friendly name of a GitHub App. # You can find this on the settings page for your GitHub App (e.g., ). diff --git a/tools/utilities/GitHubAPI.ps1 b/tools/utilities/GitHubAPI.ps1 index d07d24992..e7a06f986 100644 --- a/tools/utilities/GitHubAPI.ps1 +++ b/tools/utilities/GitHubAPI.ps1 @@ -21,7 +21,7 @@ $response = Invoke-RestMethod -Uri $APIDocURI -Method Get # @{n = 'PUT'; e = { (($_.value.psobject.Properties.Name) -contains 'PUT') } }, ` # @{n = 'PATCH'; e = { (($_.value.psobject.Properties.Name) -contains 'PATCH') } } | Format-Table -$path = '/app/hook/deliveries/{delivery_id}' +$path = '/apps/{app_slug}' $method = 'get' $response.paths.$path.$method $response.paths.$path.$method.tags | clip # -> Namespace/foldername From 74c3f0100f39f9cef5326614b0d328b296497439 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 01:09:55 +0100 Subject: [PATCH 05/29] MOve some logic :) --- src/classes/public/GitHubContext.ps1 | 4 +++ src/functions/public/API/Invoke-GitHubAPI.ps1 | 31 +++++++------------ .../public/Auth/Context/Set-GitHubContext.ps1 | 10 +++++- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/classes/public/GitHubContext.ps1 b/src/classes/public/GitHubContext.ps1 index 902375d21..b5628192b 100644 --- a/src/classes/public/GitHubContext.ps1 +++ b/src/classes/public/GitHubContext.ps1 @@ -42,6 +42,10 @@ # github.com/Octocat [string] $Name + # The context type + # User / App / Installation + [string] $Type + # The user name. [string] $UserName diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index 50f04c6ac..44d1a00a0 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -89,24 +89,14 @@ } $Context = Resolve-GitHubContext -Context $Context + $Token = $Context.Token + Write-Debug "Token : [$Token]" if ([string]::IsNullOrEmpty($TokenType)) { $TokenType = $Context.TokenType } Write-Debug "TokenType : [$($Context.TokenType)]" - switch ($TokenType) { - 'ghu' { - if (Test-GitHubAccessTokenRefreshRequired -Context $Context) { - $Token = Update-GitHubUserAccessToken -Context $Context -PassThru - } - } - 'PEM' { - $JWT = Get-GitHubAppJSONWebToken -ClientId $Context.ClientID -PrivateKey $Context.Token - $Token = $JWT.Token - } - } - if ([string]::IsNullOrEmpty($ApiBaseUri)) { $ApiBaseUri = $Context.ApiBaseUri } @@ -117,15 +107,18 @@ } Write-Debug "ApiVersion : [$($Context.ApiVersion)]" - if ([string]::IsNullOrEmpty($TokenType)) { - - } - Write-Debug "TokenType : [$($Context.TokenType)]" - if ([string]::IsNullOrEmpty($Token)) { - $Token = $Context.Token + switch ($TokenType) { + 'ghu' { + if (Test-GitHubAccessTokenRefreshRequired -Context $Context) { + $Token = Update-GitHubUserAccessToken -Context $Context -PassThru + } + } + 'PEM' { + $JWT = Get-GitHubAppJSONWebToken -ClientId $Context.ClientID -PrivateKey $Token + $Token = $JWT.Token + } } - Write-Debug "Token : [$($Context.Token)]" $headers = @{ Accept = $Accept diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 90e98bcbb..08041a40a 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -126,11 +126,18 @@ function Set-GitHubContext { 'PAT|UAT|IAT' { $viewer = Get-GitHubViewer -Context $tempContext $contextName = "$HostName/$($viewer.login)" - $context['Name'] = $contextName $context['Username'] = $viewer.login $context['NodeID'] = $viewer.id $context['DatabaseID'] = ($viewer.databaseId).ToString() } + 'PAT|UAT' { + $context['Name'] = $contextName + $context['Type'] = 'User' + } + 'IAT' { + $context['Name'] = "$contextName/$Owner" + $context['Type'] = 'Installation' + } 'App' { $app = Get-GitHubApp -Context $tempContext $contextName = "$HostName/$($app.slug)" @@ -138,6 +145,7 @@ function Set-GitHubContext { $context['Username'] = $app.slug $context['NodeID'] = $app.node_id $context['DatabaseID'] = $app.id + $context['Type'] = 'App' } default { throw 'Failed to get info on the context. Unknown logon type.' From 663ea50db9f13a0d7c457933791932f01eb9a933 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 10:01:41 +0100 Subject: [PATCH 06/29] Check to see if we can make new names on IAT creds --- src/functions/public/Auth/Context/Set-GitHubContext.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 08041a40a..0fd075d39 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -125,17 +125,18 @@ function Set-GitHubContext { switch -Regex ($AuthType) { 'PAT|UAT|IAT' { $viewer = Get-GitHubViewer -Context $tempContext - $contextName = "$HostName/$($viewer.login)" $context['Username'] = $viewer.login $context['NodeID'] = $viewer.id $context['DatabaseID'] = ($viewer.databaseId).ToString() } 'PAT|UAT' { + $contextName = "$HostName/$($viewer.login)" $context['Name'] = $contextName $context['Type'] = 'User' } 'IAT' { - $context['Name'] = "$contextName/$Owner" + $contextName = "$HostName/$($viewer.login)/$Owner" -Replace '\[bot\]' + $context['Name'] = $contextName $context['Type'] = 'Installation' } 'App' { @@ -151,7 +152,7 @@ function Set-GitHubContext { throw 'Failed to get info on the context. Unknown logon type.' } } - Write-Verbose "Found user with username: [$contextName]" + Write-Verbose "Found [$($context['Type']) with login: [$contextName]" if ($PSCmdlet.ShouldProcess('Context', 'Set')) { Set-Context -ID "$($script:Config.Name)/$contextName" -Context $context From 674defdc5e1855c6eda51947c33ab7d1c1216917 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 10:03:31 +0100 Subject: [PATCH 07/29] Dont publish --- tests/GitHub.Tests.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index ef0cf3bc9..a03e36aed 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -128,3 +128,9 @@ Describe 'GitHub' { } } } + +Context 'Fail' { + It 'Should fail' { + 1 | Should -Be 2 + } +} From 74ecacd262d844f71a9f0f5f8adaad1a907168b2 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 11:29:06 +0100 Subject: [PATCH 08/29] Fix context --- .../public/Auth/Context/Set-GitHubContext.ps1 | 7 ++++--- tests/GitHub.Tests.ps1 | 20 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 0fd075d39..d100b0868 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -125,17 +125,18 @@ function Set-GitHubContext { switch -Regex ($AuthType) { 'PAT|UAT|IAT' { $viewer = Get-GitHubViewer -Context $tempContext - $context['Username'] = $viewer.login + $login = $viewer.login + $context['Username'] = $login $context['NodeID'] = $viewer.id $context['DatabaseID'] = ($viewer.databaseId).ToString() } 'PAT|UAT' { - $contextName = "$HostName/$($viewer.login)" + $contextName = "$HostName/$login" $context['Name'] = $contextName $context['Type'] = 'User' } 'IAT' { - $contextName = "$HostName/$($viewer.login)/$Owner" -Replace '\[bot\]' + $contextName = "$HostName/$login/$Owner" -Replace '\[bot\]' $context['Name'] = $contextName $context['Type'] = 'Installation' } diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index a03e36aed..d54c4221b 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -7,6 +7,10 @@ param() Describe 'GitHub' { Context 'Auth' { + BeforeAll { + Disconnect-GitHubAccount -Context 'github.com/github-actions[bot]' -Silent + } + It 'Can connect and disconnect without parameters in GitHubActions' { { Connect-GitHubAccount } | Should -Not -Throw { Disconnect-GitHubAccount } | Should -Not -Throw @@ -26,8 +30,8 @@ Describe 'GitHub' { { Connect-GitHubAccount -Token $env:TEST_PAT } | Should -Not -Throw { Connect-GitHubAccount -Token $env:TEST_PAT } | Should -Not -Throw { Connect-GitHubAccount } | Should -Not -Throw # Logs on with GitHub Actions' token - (Get-GitHubContext -ListAvailable).Count | Should -Be 2 - Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions[bot]' + (Get-GitHubContext -ListAvailable).Count | Should -BeG 2 + Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions/PSModule' Write-Verbose (Get-GitHubContext | Out-String) -Verbose } @@ -49,7 +53,7 @@ Describe 'GitHub' { } It 'Can disconnect a specific context' { - { Disconnect-GitHubAccount -Context 'github.com/github-actions[bot]' -Silent } | Should -Not -Throw + { Disconnect-GitHubAccount -Context 'github.com/github-actions/PSModule' -Silent } | Should -Not -Throw (Get-GitHubContext -ListAvailable).Count | Should -Be 2 Connect-GitHubAccount Connect-GitHubAccount -ClientID $env:TEST_APP_CLIENT_ID -PrivateKey $env:TEST_APP_PRIVATE_KEY @@ -63,8 +67,8 @@ Describe 'GitHub' { } It 'Can swap context to another' { - { Set-GitHubDefaultContext -Context 'github.com/github-actions[bot]' } | Should -Not -Throw - Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions[bot]' + { Set-GitHubDefaultContext -Context 'github.com/github-actions/PSModule' } | Should -Not -Throw + Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions/PSModule' } # It 'Can be called with a GitHub App Installation Access Token' { @@ -128,9 +132,3 @@ Describe 'GitHub' { } } } - -Context 'Fail' { - It 'Should fail' { - 1 | Should -Be 2 - } -} From cccce9b2e5448aca92e205db68ef23e249091d81 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 11:52:23 +0100 Subject: [PATCH 09/29] Fix typo in GitHub context test assertion --- tests/GitHub.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index d54c4221b..749ed5123 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -30,7 +30,7 @@ Describe 'GitHub' { { Connect-GitHubAccount -Token $env:TEST_PAT } | Should -Not -Throw { Connect-GitHubAccount -Token $env:TEST_PAT } | Should -Not -Throw { Connect-GitHubAccount } | Should -Not -Throw # Logs on with GitHub Actions' token - (Get-GitHubContext -ListAvailable).Count | Should -BeG 2 + (Get-GitHubContext -ListAvailable).Count | Should -Be 2 Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions/PSModule' Write-Verbose (Get-GitHubContext | Out-String) -Verbose } From 62f3be55f68fb790c7aaaf05684ee0af92f5652a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 14:10:36 +0100 Subject: [PATCH 10/29] Change function to filter and update parameter attributes in Resolve-GitHubContext --- .../private/Auth/Context/Resolve-GitHubContext.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index cab5a2a0a..a3b76ecee 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -1,4 +1,4 @@ -function Resolve-GitHubContext { +filter Resolve-GitHubContext { <# .SYNOPSIS Resolves the context into a GitHubContext object. @@ -22,7 +22,7 @@ param( # The context to resolve into an object. Used to get the details for the API call. # Can be either a string or a GitHubContext object. - [Parameter()] + [Parameter(ValueFromPipeline)] [object] $Context = (Get-GitHubContext) ) @@ -44,5 +44,5 @@ throw "Context [$contextName] not found. Please provide a valid context or log in using 'Connect-GitHub'." } - return $Context + $Context } From dcf59213a1e55428cc6b8b35589be9c306408bc4 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 14:10:43 +0100 Subject: [PATCH 11/29] Add Connect-GitHubApp function for GitHub App authentication --- .../public/Auth/Connect-GitHubApp.ps1 | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/functions/public/Auth/Connect-GitHubApp.ps1 diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 new file mode 100644 index 000000000..1bde510a3 --- /dev/null +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -0,0 +1,137 @@ +function Connect-GitHubApp { + <# + .SYNOPSIS + Connects to GitHub as a installation using a GitHub App. + + .DESCRIPTION + Connects to GitHub using a GitHub App to generate installation access tokens and create contexts for targets. + + Available target types: + - User + - Organization + - Enterprise + + .EXAMPLE + Connect-GitHubApp + + Connects to GitHub as all available targets using the logged in GitHub App. + + .EXAMPLE + Connect-GitHubApp -User 'octocat' + + Connects to GitHub as the user 'octocat' using the logged in GitHub App. + + .EXAMPLE + Connect-GitHubApp -Organization 'psmodule' + + Connects to GitHub as the organization 'psmodule' using the logged in GitHub App. + + .EXAMPLE + Connect-GitHubApp -Enterprise 'msx' + + Connects to GitHub as the enterprise 'msx' using the logged in GitHub App. + + .NOTES + [Authenticating to the REST API](https://docs.github.com/rest/overview/other-authentication-methods#authenticating-for-saml-sso) + #> + [OutputType([void])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Is the CLI part of the module.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'The tokens are recieved as clear text. Mitigating exposure by removing variables and performing garbage collection.')] + [CmdletBinding(DefaultParameterSetName = '__AllParameterSets')] + param( + # The user account to connect to. + [Parameter( + Mandatory, + ParameterSetName = 'User' + )] + [string] $User, + + # The organization to connect to. + [Parameter( + Mandatory, + ParameterSetName = 'Organization' + )] + [string] $Organization, + + # The enterprise to connect to. + [Parameter( + Mandatory, + ParameterSetName = 'Enterprise' + )] + [string] $Enterprise, + + # The context to run the command in. Used to get the details for the API call. + # Can be either a string or a GitHubContext object. + [Parameter()] + [object] $Context = (Get-GitHubContext) + ) + + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose "[$commandName] - Start" + + $Context = $Context | Resolve-GitHubContext + $Context | Assert-GitHubContext -AuthType 'App' + + try { + $contextData = @{ + ApiBaseUri = $Context.ApiBaseUri + ApiVersion = $Context.ApiVersion + HostName = $Context.HostName + AuthType = 'IAT' + TokenType = 'ghs' + } + + $installations = Get-GitHubAppInstallation + switch ($PSCmdlet.ParameterSetName) { + 'User' { + $installations = $installations | Where-Object { $_.target_type -eq 'User' -and $_.account.login -in $User } + } + 'Organization' { + $installations = $installations | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -in $Organization } + } + 'Enterprise' { + $installations = $installations | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -in $Enterprise } + } + } + + foreach ($installation in $installations) { + $token = New-GitHubAppInstallationAccessToken -InstallationID $installation.id + $connectParams += @{ + Token = $token.token + TokenExpirationDate = ($token.expires_at).ToLocalTime() + } + switch ($installation.target_type) { + 'User' { + $connectParams['Owner'] = $installation.account.login + } + 'Organization' { + $connectParams['Owner'] = $installation.account.login + } + 'Enterprise' { + $connectParams['Enterprise'] = $installation.account.slug + } + } + } + + Write-Verbose 'Logging in using an installation access token...' + Write-Verbose ($contextData | Format-Table | Out-String) + $context = Set-GitHubContext @contextData -PassThru + Write-Verbose ($context | Format-List | Out-String) + if (-not $Silent) { + $name = $context.name + Write-Host "Connected $name" + } + } catch { + Write-Error $_ + Write-Error (Get-PSCallStack | Format-Table | Out-String) + throw 'Failed to connect to GitHub using a GitHub App.' + } finally { + Remove-Variable -Name tokenResponse -ErrorAction SilentlyContinue + Remove-Variable -Name context -ErrorAction SilentlyContinue + Remove-Variable -Name contextData -ErrorAction SilentlyContinue + Remove-Variable -Name Token -ErrorAction SilentlyContinue + [System.GC]::Collect() + } + Write-Verbose "[$commandName] - End" +} From 5ea03d62302e52263140ada02054d4599d17c32c Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 14:11:00 +0100 Subject: [PATCH 12/29] Add Assert-GitHubContext function to validate command context requirements --- .../public/Auth/Assert-GitHubContext.ps1 | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/functions/public/Auth/Assert-GitHubContext.ps1 diff --git a/src/functions/public/Auth/Assert-GitHubContext.ps1 b/src/functions/public/Auth/Assert-GitHubContext.ps1 new file mode 100644 index 000000000..0ee9f23e4 --- /dev/null +++ b/src/functions/public/Auth/Assert-GitHubContext.ps1 @@ -0,0 +1,35 @@ +filter Assert-GitHubContext { + <# + .SYNOPSIS + Check if the context meets the requirements for the command. + + .DESCRIPTION + This function checks if the context meets the requirements for the command. + If the context does not meet the requirements, an error is thrown. + + .EXAMPLE + Assert-GitHubContext -Context 'github.com/Octocat' -TokenType 'App' + #> + [OutputType([void])] + [CmdletBinding()] + param( + # The context to run the command in. + [Parameter( + Mandatory, + ValueFromPipeline + )] + [GitHubContext] $Context, + + # The command that is being checked. + [Parameter(Mandatory)] + [string] $Command, + + # The required authtypes for the command. + [Parameter(Mandatory)] + [string[]] $AuthType + ) + + if ($Context.AuthType -notin $AuthType) { + throw "The context '$($Context.Name)' does not match the required AuthTypes [$AuthType] for [$Command]." + } +} From f32ddf43a96d5c3428b122e961bb4dcbfaf5b09a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 14:11:09 +0100 Subject: [PATCH 13/29] Add examples for GitHub user and app authentication in CallingAPIs.ps1 --- examples/CallingAPIs.ps1 | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 examples/CallingAPIs.ps1 diff --git a/examples/CallingAPIs.ps1 b/examples/CallingAPIs.ps1 new file mode 100644 index 000000000..b1488e439 --- /dev/null +++ b/examples/CallingAPIs.ps1 @@ -0,0 +1,46 @@ +#region As a user: get the authenticated user +Connect-GitHub + +# Simple example - output is the object +Get-GitHubUser + +# More complex example - output is parts of the web response +Invoke-GitHubAPI -ApiEndpoint /user + +# Most complex example - output is the entire web response +$context = Get-GitHubContext +Invoke-WebRequest -Uri "https://api.$($context.HostName)/user" -Token ($context.Token) -Authentication Bearer +#endregion + + +#region As an app: get the authenticated app +$ClientID = '' +$PrivateKey = '' +Connect-GitHub -ClientID $ClientID -PrivateKey $PrivateKey + +# Simple example - output is the object +Get-GitHubApp + +# More complex example - output is parts of the web response +Invoke-GitHubAPI -ApiEndpoint /app + +# Most complex example - output is the entire web response +$context = Get-GitHubContext +$jwt = Get-GitHubAppJSONWebToken -ClientId $context.ClientID -PrivateKey $context.Token +Invoke-WebRequest -Uri "https://api.$($context.HostName)/user" -Token ($jwt.token) -Authentication Bearer +#endregion + + +#region As an app installation: get zen +Connect-GitHubApp -Installation 'PSModule' + +# Simple example - output is the object +Get-GitHuben + +# More complex example - output is parts of the web response +Invoke-GitHubAPI -ApiEndpoint /zen + +# Most complex example - output is the entire web response +$context = Get-GitHubContext +Invoke-WebRequest -Uri "https://api.$($context.HostName)/zen" -Token ($context.Token) -Authentication Bearer +#endregion From 3ca0003137b85bc5caeebbcdfd28ce0a43abbff5 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 15:47:30 +0100 Subject: [PATCH 14/29] Fix argument completion and token selection --- .../PowerShell/Get-FunctionParameter.ps1 | 73 +++++++++++++++++++ .../New-GitHubAppInstallationAccessToken.ps1 | 9 ++- .../public/Auth/Assert-GitHubContext.ps1 | 8 +- .../public/Auth/Connect-GitHubApp.ps1 | 62 +++++++++++----- 4 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 diff --git a/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 b/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 new file mode 100644 index 000000000..5b352b515 --- /dev/null +++ b/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 @@ -0,0 +1,73 @@ +function Get-FunctionParameter { + <# + .SYNOPSIS + Get the parameters and their final value in a function. + + .DESCRIPTION + This function retrieves the parameters and their final value in a function. + If a parameter is provided, it will retrieve the provided value. + If a parameter is not provided, it will attempt to retrieve the default value. + + .EXAMPLE + Get-FunctionParameter + + This will return all the parameters and their final value in the current function. + + .EXAMPLE + Get-FunctionParameter -IncludeCommonParameters + + This will return all the parameters and their final value in the current function, including common parameters. + + .EXAMPLE + Get-FunctionParameter -Scope 2 + + This will return all the parameters and their final value in the grandparent function. + #> + [OutputType([hashtable])] + [CmdletBinding()] + param( + # Include common parameters in the output. + [Parameter()] + [switch] $IncludeCommonParameters, + + # The function to get the parameters for. + # Default is the calling scope (1). + # Scopes are based on nesting levels: + # 0 - Current scope + # 1 - Parent scope + # 2 - Grandparent scope + [Parameter()] + [string] $Scope = 1 + ) + + $commonParameters = @( + 'ProgressAction', 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', + 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'WhatIf', + 'Confirm' + ) + + $InvocationInfo = (Get-Variable -Name MyInvocation -Scope $Scope -ErrorAction Stop).Value + $boundParameters = $InvocationInfo.BoundParameters + $allParameters = @{} + $parameters = $InvocationInfo.MyCommand.Parameters + + foreach ($paramName in $parameters.Keys) { + if (-not $IncludeCommonParameters -and $paramName -in $commonParameters) { + continue + } + if ($boundParameters.ContainsKey($paramName)) { + # Use the explicitly provided value + $allParameters[$paramName] = $boundParameters[$paramName] + } else { + # Attempt to retrieve the default value by invoking it + try { + $defaultValue = (Get-Variable -Name $paramName -Scope $Scope -ErrorAction SilentlyContinue).Value + } catch { + $defaultValue = $null + } + $allParameters[$paramName] = $defaultValue + } + } + + $allParameters +} diff --git a/src/functions/public/Apps/GitHub Apps/New-GitHubAppInstallationAccessToken.ps1 b/src/functions/public/Apps/GitHub Apps/New-GitHubAppInstallationAccessToken.ps1 index 8a0613de8..d4d3667e3 100644 --- a/src/functions/public/Apps/GitHub Apps/New-GitHubAppInstallationAccessToken.ps1 +++ b/src/functions/public/Apps/GitHub Apps/New-GitHubAppInstallationAccessToken.ps1 @@ -45,6 +45,8 @@ 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'No state is changed.' )] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'The tokens are recieved as clear text. Mitigating exposure by removing variables and performing garbage collection.')] [CmdletBinding()] param( # The unique identifier of the installation. @@ -72,6 +74,11 @@ } Invoke-GitHubAPI @inputObject | ForEach-Object { - Write-Output $_.Response + [pscustomobject]@{ + Token = $_.Response.token | ConvertTo-SecureString -AsPlainText -Force + ExpiresAt = $_.Response.expires_at.ToLocalTime() + Permissions = $_.Response.permissions + RepositorySelection = $_.Response.repository_selection + } } } diff --git a/src/functions/public/Auth/Assert-GitHubContext.ps1 b/src/functions/public/Auth/Assert-GitHubContext.ps1 index 0ee9f23e4..e53b67ae0 100644 --- a/src/functions/public/Auth/Assert-GitHubContext.ps1 +++ b/src/functions/public/Auth/Assert-GitHubContext.ps1 @@ -20,16 +20,14 @@ )] [GitHubContext] $Context, - # The command that is being checked. - [Parameter(Mandatory)] - [string] $Command, - # The required authtypes for the command. [Parameter(Mandatory)] [string[]] $AuthType ) + $command = (Get-PSCallStack)[1].Command + if ($Context.AuthType -notin $AuthType) { - throw "The context '$($Context.Name)' does not match the required AuthTypes [$AuthType] for [$Command]." + throw "The context '$($Context.Name)' does not match the required AuthTypes [$AuthType] for [$command]." } } diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index 1bde510a3..067db62ad 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -74,7 +74,7 @@ $Context | Assert-GitHubContext -AuthType 'App' try { - $contextData = @{ + $defaultContextData = @{ ApiBaseUri = $Context.ApiBaseUri ApiVersion = $Context.ApiVersion HostName = $Context.HostName @@ -96,42 +96,66 @@ } foreach ($installation in $installations) { + $contextParams = $defaultContextData.Clone() $token = New-GitHubAppInstallationAccessToken -InstallationID $installation.id - $connectParams += @{ - Token = $token.token - TokenExpirationDate = ($token.expires_at).ToLocalTime() + $contextParams += @{ + Token = $token.Token + TokenExpirationDate = $token.ExpiresAt } switch ($installation.target_type) { 'User' { - $connectParams['Owner'] = $installation.account.login + $contextParams['Owner'] = $installation.account.login } 'Organization' { - $connectParams['Owner'] = $installation.account.login + $contextParams['Owner'] = $installation.account.login } 'Enterprise' { - $connectParams['Enterprise'] = $installation.account.slug + $contextParams['Enterprise'] = $installation.account.slug } } + Write-Verbose 'Logging in using an installation access token...' + Write-Verbose ($contextParams | Format-Table | Out-String) + $tmpContext = Set-GitHubContext @contextParams -PassThru + Write-Verbose ($tmpContext | Format-List | Out-String) + if (-not $Silent) { + $name = $tmpContext.name + Write-Host "Connected $name" + } + Remove-Variable -Name tmpContext -ErrorAction SilentlyContinue + Remove-Variable -Name token -ErrorAction SilentlyContinue } - Write-Verbose 'Logging in using an installation access token...' - Write-Verbose ($contextData | Format-Table | Out-String) - $context = Set-GitHubContext @contextData -PassThru - Write-Verbose ($context | Format-List | Out-String) - if (-not $Silent) { - $name = $context.name - Write-Host "Connected $name" - } } catch { Write-Error $_ Write-Error (Get-PSCallStack | Format-Table | Out-String) throw 'Failed to connect to GitHub using a GitHub App.' } finally { - Remove-Variable -Name tokenResponse -ErrorAction SilentlyContinue - Remove-Variable -Name context -ErrorAction SilentlyContinue - Remove-Variable -Name contextData -ErrorAction SilentlyContinue - Remove-Variable -Name Token -ErrorAction SilentlyContinue [System.GC]::Collect() } Write-Verbose "[$commandName] - End" } + +Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName User -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter + + Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'User' -and $_.account.login -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) + } +} +Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName Organization -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter + + Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) + } +} +Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName Enterprise -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) + $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter + + Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.slug, $_.account.slug, 'ParameterValue', $_.account.slug) + } +} From 424e3eadd09e1e0b811844ca8f4b2216ed1f802d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 16:27:19 +0100 Subject: [PATCH 15/29] Add ClientID to authentication context in Connect-GitHubApp function --- src/functions/public/Auth/Connect-GitHubApp.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index 067db62ad..c22bc088b 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -78,6 +78,7 @@ ApiBaseUri = $Context.ApiBaseUri ApiVersion = $Context.ApiVersion HostName = $Context.HostName + ClientID = $Context.AuthClientID AuthType = 'IAT' TokenType = 'ghs' } From 39b9fc17e7978072f00cea3811bd819107b68f4f Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 16:27:35 +0100 Subject: [PATCH 16/29] Refactor parameter logging in Invoke-GitHubAPI function --- src/functions/public/API/Invoke-GitHubAPI.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index 44d1a00a0..17f368c28 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -84,7 +84,7 @@ Write-Debug 'Invoking GitHub API...' Write-Debug 'Parameters:' - $PSBoundParameters.GetEnumerator() | ForEach-Object { + Get-FunctionParameter | ForEach-Object { Write-Debug " - $($_.Key): $($_.Value)" } From 5d91873ca5971a346f0edb4786824577eec5c4f3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 17:17:49 +0100 Subject: [PATCH 17/29] Change output type to pscustomobject and adjust default scope in Get-FunctionParameter --- .../Utilities/PowerShell/Get-FunctionParameter.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 b/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 index 5b352b515..2adb448de 100644 --- a/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 +++ b/src/functions/private/Utilities/PowerShell/Get-FunctionParameter.ps1 @@ -23,7 +23,7 @@ This will return all the parameters and their final value in the grandparent function. #> - [OutputType([hashtable])] + [OutputType([pscustomobject])] [CmdletBinding()] param( # Include common parameters in the output. @@ -31,15 +31,17 @@ [switch] $IncludeCommonParameters, # The function to get the parameters for. - # Default is the calling scope (1). + # Default is the calling scope (0). # Scopes are based on nesting levels: # 0 - Current scope # 1 - Parent scope # 2 - Grandparent scope [Parameter()] - [string] $Scope = 1 + [int] $Scope = 0 ) + $Scope++ + $commonParameters = @( 'ProgressAction', 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'Verbose', 'WarningAction', 'WarningVariable', 'WhatIf', @@ -69,5 +71,5 @@ } } - $allParameters + [pscustomobject]$allParameters } From 2ceeab4c17039981868808c7ed1193bfbc7f59f3 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 17:17:58 +0100 Subject: [PATCH 18/29] Refactor GitHub context completion to use Name property for parameter suggestions --- src/completers.ps1 | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/completers.ps1 b/src/completers.ps1 index bc6671feb..1b742bed0 100644 --- a/src/completers.ps1 +++ b/src/completers.ps1 @@ -2,10 +2,7 @@ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - $contexts = Get-GitHubContext -ListAvailable -Verbose:$false | Where-Object { "$($_.HostName)/$($_.UserName)" -like "$wordToComplete*" } - - $contexts | ForEach-Object { - $contextID = "$($_.HostName)/$($_.UserName)" - [System.Management.Automation.CompletionResult]::new($contextID, $contextID, 'ParameterValue', $contextID) + Get-GitHubContext -ListAvailable -Verbose:$false | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Name) } } From 4f917f3a4248ceac8ddffa30fc8bd19e00ffe5ed Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 17:18:13 +0100 Subject: [PATCH 19/29] Optimize installation retrieval in Connect-GitHubApp with parallel processing and suppress verbose output --- .../public/Auth/Connect-GitHubApp.ps1 | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index c22bc088b..6ba0a7904 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -96,7 +96,8 @@ } } - foreach ($installation in $installations) { + $installations | ForEach-Object -ThrottleLimit [System.Environment]::ProcessorCount -Parallel { + $installation = $_ $contextParams = $defaultContextData.Clone() $token = New-GitHubAppInstallationAccessToken -InstallationID $installation.id $contextParams += @{ @@ -140,23 +141,26 @@ Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName User -S param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'User' -and $_.account.login -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) - } + Get-GitHubAppInstallation -Verbose:$false | Where-Object { $_.target_type -eq 'User' -and $_.account.login -like "$wordToComplete*" } | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) + } } Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName Organization -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) - } + Get-GitHubAppInstallation -Verbose:$false | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -like "$wordToComplete*" } | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.login, $_.account.login, 'ParameterValue', $_.account.login) + } } Register-ArgumentCompleter -CommandName Connect-GitHubApp -ParameterName Enterprise -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter - Get-GitHubAppInstallation | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -like "$wordToComplete*" } | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_.account.slug, $_.account.slug, 'ParameterValue', $_.account.slug) - } + Get-GitHubAppInstallation -Verbose:$false | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -like "$wordToComplete*" } | + ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_.account.slug, $_.account.slug, 'ParameterValue', $_.account.slug) + } } From 9855202d7b0f28eb324b76be36a632a856db314d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 17:21:43 +0100 Subject: [PATCH 20/29] Improve debug output in Invoke-GitHubAPI by formatting parameters for better readability --- src/functions/public/API/Invoke-GitHubAPI.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index 17f368c28..643ab7407 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -84,9 +84,8 @@ Write-Debug 'Invoking GitHub API...' Write-Debug 'Parameters:' - Get-FunctionParameter | ForEach-Object { - Write-Debug " - $($_.Key): $($_.Value)" - } + Get-FunctionParameter | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } + Get-FunctionParameter -Scope 1 | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } $Context = Resolve-GitHubContext -Context $Context $Token = $Context.Token From 2029994a6987d857f6db0370f3c6df56bb744f6d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 17:22:49 +0100 Subject: [PATCH 21/29] Enhance debug output in Invoke-GitHubAPI to include parent function parameters for improved traceability --- src/functions/public/API/Invoke-GitHubAPI.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/functions/public/API/Invoke-GitHubAPI.ps1 b/src/functions/public/API/Invoke-GitHubAPI.ps1 index 643ab7407..5062b0b3d 100644 --- a/src/functions/public/API/Invoke-GitHubAPI.ps1 +++ b/src/functions/public/API/Invoke-GitHubAPI.ps1 @@ -85,6 +85,7 @@ Write-Debug 'Invoking GitHub API...' Write-Debug 'Parameters:' Get-FunctionParameter | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } + Write-Debug 'Parent function parameters:' Get-FunctionParameter -Scope 1 | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ } $Context = Resolve-GitHubContext -Context $Context From 8117936ab6561195958cb02cc057fec94f0316c7 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 18:49:30 +0100 Subject: [PATCH 22/29] Add InstallationID parameter to GitHub context and update related functions --- src/classes/public/GitHubContext.ps1 | 3 +++ src/functions/public/Auth/Connect-GitHubApp.ps1 | 6 +++--- src/functions/public/Auth/Context/Set-GitHubContext.ps1 | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/classes/public/GitHubContext.ps1 b/src/classes/public/GitHubContext.ps1 index b5628192b..f51de848e 100644 --- a/src/classes/public/GitHubContext.ps1 +++ b/src/classes/public/GitHubContext.ps1 @@ -73,6 +73,9 @@ # 2024-01-01-00:00:00 [datetime] $TokenExpirationDate + # The installation ID. + [int] $InstallationID + # The refresh token. [securestring] $RefreshToken diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index 6ba0a7904..7b70f7884 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -96,11 +96,12 @@ } } - $installations | ForEach-Object -ThrottleLimit [System.Environment]::ProcessorCount -Parallel { + $installations | ForEach-Object { $installation = $_ - $contextParams = $defaultContextData.Clone() + $contextParams = @{} + $defaultContextData.Clone() $token = New-GitHubAppInstallationAccessToken -InstallationID $installation.id $contextParams += @{ + InstallationID = $installation.id Token = $token.Token TokenExpirationDate = $token.ExpiresAt } @@ -126,7 +127,6 @@ Remove-Variable -Name tmpContext -ErrorAction SilentlyContinue Remove-Variable -Name token -ErrorAction SilentlyContinue } - } catch { Write-Error $_ Write-Error (Get-PSCallStack | Format-Table | Out-String) diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index d100b0868..3802a4319 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -57,6 +57,9 @@ function Set-GitHubContext { [Parameter(Mandatory)] [string] $HostName, + # Set the installation ID. + [int] $InstallationID, + # Set the enterprise name for the context. [Parameter()] [string] $Enterprise, From ad123faa5905836d8e56cae4eccf54e935f4584a Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 19:12:30 +0100 Subject: [PATCH 23/29] Refactor GitHub context format to use 'Name' property and update Connect-GitHubApp to align with new context structure --- src/formats/GitHubContext.Format.ps1xml | 22 ++----------------- .../public/Auth/Connect-GitHubApp.ps1 | 2 +- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/src/formats/GitHubContext.Format.ps1xml b/src/formats/GitHubContext.Format.ps1xml index 14fef32a3..638e3d673 100644 --- a/src/formats/GitHubContext.Format.ps1xml +++ b/src/formats/GitHubContext.Format.ps1xml @@ -10,10 +10,7 @@ - - - - + @@ -24,21 +21,12 @@ - - - - - - - UserName - - - HostName + Name AuthType @@ -49,12 +37,6 @@ TokenExpirationDate - - Owner - - - Repo - diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index 7b70f7884..d541a2d99 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -78,7 +78,7 @@ ApiBaseUri = $Context.ApiBaseUri ApiVersion = $Context.ApiVersion HostName = $Context.HostName - ClientID = $Context.AuthClientID + ClientID = $Context.ClientID AuthType = 'IAT' TokenType = 'ghs' } From 441389cff6d3e4c568d53d110283b1d7e59725f9 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 19:18:43 +0100 Subject: [PATCH 24/29] Add InstallationID parameter to Set-GitHubContext for enhanced context configuration --- src/functions/public/Auth/Context/Set-GitHubContext.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 3802a4319..73e88f756 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -58,6 +58,7 @@ function Set-GitHubContext { [string] $HostName, # Set the installation ID. + [Parameter()] [int] $InstallationID, # Set the enterprise name for the context. @@ -105,6 +106,7 @@ function Set-GitHubContext { AuthClientID = $AuthClientID # Client ID for UAT AuthType = $AuthType # UAT / PAT / App / IAT ClientID = $ClientID # Client ID for GitHub Apps + InstallationID = $InstallationID # Installation ID DeviceFlowType = $DeviceFlowType # GitHubApp / OAuthApp HostName = $HostName # github.com / msx.ghe.com / github.local Enterprise = $Enterprise # Enterprise name From 64376434b7c348e87d846e42dd0bb12814df8eac Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 19:42:40 +0100 Subject: [PATCH 25/29] Adding a function in the resolver to return installations for targeted functions --- .../Auth/Context/Resolve-GitHubContext.ps1 | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index a3b76ecee..b9843cd3d 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -5,7 +5,10 @@ .DESCRIPTION This function resolves the context into a GitHubContext object. - It can take both the + If the context is already a GitHubContext object, it will return the object. + If the context is a string, it will get the context details and return a GitHubContext object. + + If the context is a App, it will look at the available contexts and return the one that matches the scope of the command being run. .EXAMPLE $Context = Resolve-GitHubContext -Context 'github.com/Octocat' @@ -44,5 +47,20 @@ throw "Context [$contextName] not found. Please provide a valid context or log in using 'Connect-GitHub'." } + switch ($Context.Type) { + 'App' { + $availableContexts = Get-GitHubContext -ListAvailable | Where-Object { $_.Type -eq 'Installation' -and $_.ClientID -eq $Context.ClientID } + $params = Get-FunctionParameter -Scope 2 + Write-Verbose "Resolving parameters used in called function" + Write-Verbose ($params | Out-String) + if ($params.Keys -in 'Owner', 'Organization') { + $foundContext = $availableContexts | Where-Object { $_.Type -eq 'Installation' -and $_.Owner -eq $params.Owner } + if ($foundContext) { + return $foundContext + } + } + } + } + $Context } From 86f99c0624acbfc5a18d649ea1148ffb5cae59df Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 19:43:52 +0100 Subject: [PATCH 26/29] Fix --- src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index b9843cd3d..d03b5c376 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -29,10 +29,6 @@ [object] $Context = (Get-GitHubContext) ) - if ($Context -is [GitHubContext]) { - return $Context - } - if ([string]::IsNullOrEmpty($Context)) { throw "No contexts has been specified. Please provide a context or log in using 'Connect-GitHub'." } From 3cf4a746d2c2ae49c6c2ababa95ac566738f18fb Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 20:21:17 +0100 Subject: [PATCH 27/29] Refactor Resolve-GitHubContext to improve context validation and logging --- .../Auth/Context/Resolve-GitHubContext.ps1 | 52 +++++++++++-------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index d03b5c376..560abf23b 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -29,34 +29,44 @@ [object] $Context = (Get-GitHubContext) ) - if ([string]::IsNullOrEmpty($Context)) { - throw "No contexts has been specified. Please provide a context or log in using 'Connect-GitHub'." + begin { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose "[$commandName] - Start" + Write-Verbose "Context:" + Write-Verbose ($Context | Out-String) } - if ($Context -is [string]) { - $contextName = $Context - Write-Debug "Getting context: [$contextName]" - $Context = Get-GitHubContext -Context $contextName - } + process { + if ([string]::IsNullOrEmpty($Context)) { + throw "No contexts has been specified. Please provide a context or log in using 'Connect-GitHub'." + } - if (-not $Context) { - throw "Context [$contextName] not found. Please provide a valid context or log in using 'Connect-GitHub'." - } + if ($Context -is [string]) { + $contextName = $Context + Write-Debug "Getting context: [$contextName]" + $Context = Get-GitHubContext -Context $contextName + } - switch ($Context.Type) { - 'App' { - $availableContexts = Get-GitHubContext -ListAvailable | Where-Object { $_.Type -eq 'Installation' -and $_.ClientID -eq $Context.ClientID } - $params = Get-FunctionParameter -Scope 2 - Write-Verbose "Resolving parameters used in called function" - Write-Verbose ($params | Out-String) - if ($params.Keys -in 'Owner', 'Organization') { - $foundContext = $availableContexts | Where-Object { $_.Type -eq 'Installation' -and $_.Owner -eq $params.Owner } - if ($foundContext) { - return $foundContext + if (-not $Context) { + throw "Context [$contextName] not found. Please provide a valid context or log in using 'Connect-GitHub'." + } + + switch ($Context.Type) { + 'App' { + $availableContexts = Get-GitHubContext -ListAvailable | + Where-Object { $_.Type -eq 'Installation' -and $_.ClientID -eq $Context.ClientID } + $params = Get-FunctionParameter -Scope 2 + Write-Verbose 'Resolving parameters used in called function' + Write-Verbose ($params | Out-String) + if ($params.Keys -in 'Owner', 'Organization') { + $Context = $availableContexts | Where-Object { $_.Owner -eq $params.Owner } } } } } - $Context + end { + Write-Output $Context + Write-Verbose "[$commandName] - End" + } } From 833207902e97d4d2777d03db654d349c6276fe05 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 20:40:18 +0100 Subject: [PATCH 28/29] Test --- .../private/Auth/Context/Resolve-GitHubContext.ps1 | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 index 560abf23b..716846345 100644 --- a/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 +++ b/src/functions/private/Auth/Context/Resolve-GitHubContext.ps1 @@ -32,15 +32,11 @@ begin { $commandName = $MyInvocation.MyCommand.Name Write-Verbose "[$commandName] - Start" - Write-Verbose "Context:" - Write-Verbose ($Context | Out-String) + Write-Verbose 'Context:' + $Context | Out-String -Stream | ForEach-Object { Write-Verbose $_ } } process { - if ([string]::IsNullOrEmpty($Context)) { - throw "No contexts has been specified. Please provide a context or log in using 'Connect-GitHub'." - } - if ($Context -is [string]) { $contextName = $Context Write-Debug "Getting context: [$contextName]" @@ -66,7 +62,7 @@ } end { - Write-Output $Context Write-Verbose "[$commandName] - End" + Write-Output $Context } } From aa128c1d9681235b6c561a05a5a2d9745b138749 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 6 Dec 2024 23:59:39 +0100 Subject: [PATCH 29/29] Add tests for connecting to GitHub App installations --- tests/GitHub.Tests.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index 749ed5123..c917007c0 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -66,6 +66,16 @@ Describe 'GitHub' { $app | Should -Not -BeNullOrEmpty } + It 'Can connect to a GitHub App Installation' { + { Connect-GitHubApp -Organization 'PSModule' } | Should -Not -Throw + Write-Verbose (Get-GitHubContext | Out-String) -Verbose + } + + It 'Can connect to all GitHub App Installations' { + { Connect-GitHubApp } | Should -Not -Throw + Write-Verbose (Get-GitHubContext | Out-String) -Verbose + } + It 'Can swap context to another' { { Set-GitHubDefaultContext -Context 'github.com/github-actions/PSModule' } | Should -Not -Throw Get-GitHubConfig -Name 'DefaultContext' | Should -Be 'github.com/github-actions/PSModule'