diff --git a/Coverage.md b/Coverage.md index 984361d8a..5dc35128d 100644 --- a/Coverage.md +++ b/Coverage.md @@ -5,7 +5,7 @@ - + @@ -13,11 +13,11 @@ - + - +
Available functions985980
Covered functions
Missing functions830825
Coverage15.74%15.82%
@@ -52,13 +52,8 @@ | `/codes_of_conduct` | | :x: | | | | | `/codes_of_conduct/{key}` | | :x: | | | | | `/emojis` | | :white_check_mark: | | | | -| `/enterprises/{enterprise}/copilot/billing/seats` | | :x: | | | | -| `/enterprises/{enterprise}/copilot/metrics` | | :x: | | | | -| `/enterprises/{enterprise}/copilot/usage` | | :x: | | | | | `/enterprises/{enterprise}/dependabot/alerts` | | :x: | | | | | `/enterprises/{enterprise}/secret-scanning/alerts` | | :x: | | | | -| `/enterprises/{enterprise}/team/{team_slug}/copilot/metrics` | | :x: | | | | -| `/enterprises/{enterprise}/team/{team_slug}/copilot/usage` | | :x: | | | | | `/events` | | :x: | | | | | `/feeds` | | :x: | | | | | `/gists` | | :x: | | :x: | | diff --git a/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 b/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 index 3bd58725f..68a010afd 100644 --- a/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext/AppGitHubContext.ps1 @@ -9,11 +9,14 @@ [string] $OwnerType # The permissions that the app is requesting on the target - [string[]] $Permissions + [pscustomobject] $Permissions # The events that the app is subscribing to once installed [string[]] $Events + # Simple parameterless constructor + AppGitHubContext() {} + # Creates a context object from a hashtable of key-vaule pairs. AppGitHubContext([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { diff --git a/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 b/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 index d4a97368c..ed02de33c 100644 --- a/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1 @@ -10,7 +10,7 @@ [int] $InstallationID # The permissions that the app is requesting on the target - [string[]] $Permissions + [pscustomobject] $Permissions # The events that the app is subscribing to once installed [string[]] $Events @@ -21,6 +21,9 @@ # The target login of the installation. [string] $TargetName + # Simple parameterless constructor + InstallationGitHubContext() {} + # Creates a context object from a hashtable of key-vaule pairs. InstallationGitHubContext([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { diff --git a/src/classes/public/Context/GitHubContext/UserGitHubContext.ps1 b/src/classes/public/Context/GitHubContext/UserGitHubContext.ps1 index d0fa35ebf..345aaf867 100644 --- a/src/classes/public/Context/GitHubContext/UserGitHubContext.ps1 +++ b/src/classes/public/Context/GitHubContext/UserGitHubContext.ps1 @@ -22,6 +22,9 @@ # 2024-01-01-00:00:00 [datetime] $RefreshTokenExpirationDate + # Simple parameterless constructor + UserGitHubContext() {} + # Creates a context object from a hashtable of key-vaule pairs. UserGitHubContext([hashtable]$Properties) { foreach ($Property in $Properties.Keys) { diff --git a/src/functions/public/Auth/Connect-GitHubAccount.ps1 b/src/functions/public/Auth/Connect-GitHubAccount.ps1 index ffac8c5ac..725b6338e 100644 --- a/src/functions/public/Auth/Connect-GitHubAccount.ps1 +++ b/src/functions/public/Auth/Connect-GitHubAccount.ps1 @@ -103,17 +103,11 @@ )] [string] $PrivateKey, - # Skip loading GitHub App contexts. + # Automatically load installations for the GitHub App. [Parameter( ParameterSetName = 'App' )] - [switch] $SkipAppAutoload, - - # Do not load credentials for the GitHub App Installations, just metadata. - [Parameter( - ParameterSetName = 'App' - )] - [switch] $Shallow, + [switch] $AutoloadInstallations, # The default enterprise to use in commands. [Parameter()] @@ -181,19 +175,6 @@ if (-not $customTokenProvided -and $gitHubTokenPresent) { $authType = 'Token' $Token = $gitHubToken - $gitHubEvent = Get-Content -Path $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json - 'Enterprise: ' + $gitHubEvent.enterprise.slug - 'Organization: ' + $gitHubEvent.organization.login - 'Repository: ' + $gitHubEvent.repository.name - 'Repository Owner: ' + $gitHubEvent.repository.owner.login - 'Repository Owner Type: ' + $gitHubEvent.repository.owner.type - 'Sender: ' + $gitHubEvent.sender.login - - $Enterprise = [string]$gitHubEvent.enterprise.slug - $TargetType = [string]$gitHubEvent.repository.owner.type - $TargetName = [string]$gitHubEvent.repository.owner.login - $Owner = [string]$gitHubEvent.repository.owner.login - $Repo = [string]$gitHubEvent.repository.name } } @@ -299,10 +280,8 @@ 'ghs' { Write-Verbose 'Logging in using an installation access token...' $context += @{ - Token = ConvertTo-SecureString -AsPlainText $Token - TokenType = $tokenType - TargetType = $TargetType - TargetName = $TargetName + Token = ConvertTo-SecureString -AsPlainText $Token + TokenType = $tokenType } $context['AuthType'] = 'IAT' } @@ -322,9 +301,9 @@ Write-Host "Logged in as $name!" } - if ($authType -eq 'App' -and -not $SkipAppAutoload) { - Write-Verbose 'Loading GitHub App contexts...' - Connect-GitHubApp -Shallow:$Shallow + if ($authType -eq 'App' -and $AutoloadInstallations) { + Write-Verbose 'Loading GitHub App Installation contexts...' + Connect-GitHubApp } } catch { diff --git a/src/functions/public/Auth/Connect-GitHubApp.ps1 b/src/functions/public/Auth/Connect-GitHubApp.ps1 index c6751d7da..498ab089b 100644 --- a/src/functions/public/Auth/Connect-GitHubApp.ps1 +++ b/src/functions/public/Auth/Connect-GitHubApp.ps1 @@ -64,96 +64,86 @@ # 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), - - # Do not load credentials for the GitHub App Installations, just metadata. - [Parameter()] - [switch] $Shallow + [object] $Context = (Get-GitHubContext) ) - $commandName = $MyInvocation.MyCommand.Name - Write-Verbose "[$commandName] - Start" + begin { + $commandName = $MyInvocation.MyCommand.Name + Write-Verbose "[$commandName] - Start" + } - $Context = $Context | Resolve-GitHubContext - $Context | Assert-GitHubContext -AuthType 'App' + process { + try { + $Context = $Context | Resolve-GitHubContext + $Context | Assert-GitHubContext -AuthType 'App' - try { - $defaultContextData = @{ - ApiBaseUri = $Context.ApiBaseUri - ApiVersion = $Context.ApiVersion - HostName = $Context.HostName - ClientID = $Context.ClientID - AuthType = 'IAT' - TokenType = 'ghs' - } - - $installations = Get-GitHubAppInstallation - Write-Verbose "Found [$($installations.Count)] installations." - switch ($PSCmdlet.ParameterSetName) { - 'User' { - Write-Verbose "Filtering installations for user [$User]." - $installations = $installations | Where-Object { $_.target_type -eq 'User' -and $_.account.login -in $User } - } - 'Organization' { - Write-Verbose "Filtering installations for organization [$Organization]." - $installations = $installations | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -in $Organization } - } - 'Enterprise' { - Write-Verbose "Filtering installations for enterprise [$Enterprise]." - $installations = $installations | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -in $Enterprise } - } - } - - Write-Verbose "Found [$($installations.Count)] installations for the target type." - $installations | ForEach-Object { - $installation = $_ - $contextParams = @{} + $defaultContextData.Clone() - if ($Shallow) { - $token = [PSCustomObject]@{ - Token = [securestring]::new() - ExpiresAt = [datetime]::MinValue - } - } else { - $token = New-GitHubAppInstallationAccessToken -InstallationID $installation.id - } - $contextParams += @{ - InstallationID = $installation.id - Token = $token.Token - TokenExpirationDate = $token.ExpiresAt - Permissions = $installation.permissions - Events = $installation.events - TargetType = $installation.target_type - } - switch ($installation.target_type) { + $installations = Get-GitHubAppInstallation -Context $Context + Write-Verbose "Found [$($installations.Count)] installations." + switch ($PSCmdlet.ParameterSetName) { 'User' { - $contextParams += @{ - TargetName = $installation.account.login - } + Write-Verbose "Filtering installations for user [$User]." + $installations = $installations | Where-Object { $_.target_type -eq 'User' -and $_.account.login -in $User } } 'Organization' { - $contextParams += @{ - TargetName = $installation.account.login - } + Write-Verbose "Filtering installations for organization [$Organization]." + $installations = $installations | Where-Object { $_.target_type -eq 'Organization' -and $_.account.login -in $Organization } } 'Enterprise' { - $contextParams += @{ - TargetName = $installation.account.slug - } + Write-Verbose "Filtering installations for enterprise [$Enterprise]." + $installations = $installations | Where-Object { $_.target_type -eq 'Enterprise' -and $_.account.slug -in $Enterprise } } } - Write-Verbose 'Logging in using an installation access token...' - Write-Verbose ($contextParams | Format-Table | Out-String) - $tmpContext = [InstallationGitHubContext]::new((Set-GitHubContext -Context $contextParams -PassThru)) - Write-Verbose ($tmpContext | Format-List | Out-String) - if (-not $Silent) { - $name = $tmpContext.name - Write-Host "Connected $name" + + Write-Verbose "Found [$($installations.Count)] installations for the target." + $installations | ForEach-Object { + $installation = $_ + Write-Verbose "Processing installation [$($installation.account.login)] [$($installation.id)]" + $token = New-GitHubAppInstallationAccessToken -Context $Context -InstallationID $installation.id + + $contextParams = @{ + AuthType = [string]'IAT' + TokenType = [string]'ghs' + DisplayName = [string]$Context.DisplayName + ApiBaseUri = [string]$Context.ApiBaseUri + ApiVersion = [string]$Context.ApiVersion + HostName = [string]$Context.HostName + ClientID = [string]$Context.ClientID + InstallationID = [string]$installation.id + Permissions = [pscustomobject]$installation.permissions + Events = [string[]]$installation.events + TargetType = [string]$installation.target_type + Token = [securestring]$token.Token + TokenExpirationDate = [string]$token.ExpiresAt + } + + switch ($installation.target_type) { + 'User' { + $contextParams['TargetName'] = $installation.account.login + } + 'Organization' { + $contextParams['TargetName'] = $installation.account.login + } + 'Enterprise' { + $contextParams['TargetName'] = $installation.account.slug + } + } + Write-Verbose 'Logging in using a managed installation access token...' + Write-Verbose ($contextParams | Format-Table | Out-String) + $tmpContext = [InstallationGitHubContext]::new((Set-GitHubContext -Context $contextParams.Clone() -PassThru)) + Write-Verbose ($tmpContext | Format-List | Out-String) + if (-not $Silent) { + $name = $tmpContext.name + Write-Host "Connected $name" + } + $contextParams.Clear() } + } catch { + Write-Error $_ + Write-Error (Get-PSCallStack | Format-Table | Out-String) + throw 'Failed to connect to GitHub using a GitHub App.' } - } catch { - Write-Error $_ - Write-Error (Get-PSCallStack | Format-Table | Out-String) - throw 'Failed to connect to GitHub using a GitHub App.' } - Write-Verbose "[$commandName] - End" + end { + Write-Verbose "[$commandName] - End" + } } diff --git a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 index 590e5983f..cb420df53 100644 --- a/src/functions/public/Auth/Context/Set-GitHubContext.ps1 +++ b/src/functions/public/Auth/Context/Set-GitHubContext.ps1 @@ -42,80 +42,124 @@ function Set-GitHubContext { $commandName = $MyInvocation.MyCommand.Name Write-Verbose "[$commandName] - Start" $null = Get-GitHubConfig + $contextObj = @{} + $Context } process { Write-Verbose 'Context:' - $Context | Out-String -Stream | ForEach-Object { Write-Verbose $_ } + $contextObj | Out-String -Stream | ForEach-Object { Write-Verbose $_ } # Run functions to get info on the temporary context. try { - Write-Verbose "Getting info on the context [$($Context['AuthType'])]." - switch -Regex ($($Context['AuthType'])) { + Write-Verbose "Getting info on the context [$($contextObj['AuthType'])]." + switch -Regex ($($contextObj['AuthType'])) { 'PAT|UAT|IAT' { - $viewer = Get-GitHubViewer -Context $Context + $viewer = Get-GitHubViewer -Context $contextObj $viewer | Out-String -Stream | ForEach-Object { Write-Verbose $_ } - $login = [string]$viewer.login - $Context += @{ - DisplayName = [string]$viewer.name - Username = $login - NodeID = [string]$viewer.id - DatabaseID = [string]$viewer.databaseId + if ([string]::IsNullOrEmpty($contextObj['DisplayName'])) { + $contextObj['DisplayName'] = [string]$viewer.name + } + if ([string]::IsNullOrEmpty($contextObj['Username'])) { + $login = [string]($viewer.login -Replace '\[bot\]') + $contextObj['Username'] = $login + } + if ([string]::IsNullOrEmpty($contextObj['NodeID'])) { + $contextObj['NodeID'] = [string]$viewer.id + } + if ([string]::IsNullOrEmpty($contextObj['DatabaseID'])) { + $contextObj['DatabaseID'] = [string]$viewer.databaseId } } 'PAT|UAT' { - $ContextName = "$($Context['HostName'])/$login" - $Context += @{ - Name = $ContextName - Type = 'User' - } + $contextName = "$($contextObj['HostName'])/$login" + $contextObj['Name'] = $contextName + $contextObj['Type'] = 'User' } 'IAT' { - $ContextName = "$($Context['HostName'])/$login/$($Context.TargetType)/$($Context.TargetName)" -Replace '\[bot\]' - $Context += @{ - Name = $ContextName - Type = 'Installation' + $contextObj['Type'] = 'Installation' + if ([string]::IsNullOrEmpty($contextObj['DisplayName'])) { + try { + $app = Get-GitHubApp -AppSlug $contextObj['Username'] -Context $contextObj + $contextObj['DisplayName'] = [string]$app.name + } catch { + Write-Warning "Failed to get the GitHub App with the slug: [$($contextObj['Username'])]." + } + } + if ($script:GitHub.EnvironmentType -eq 'GHA') { + $gitHubEvent = Get-Content -Path $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json + $targetType = $gitHubEvent.repository.owner.type + $targetName = $gitHubEvent.repository.owner.login + $enterprise = $gitHubEvent.enterprise.slug + $organization = $gitHubEvent.organization.login + $owner = $gitHubEvent.repository.owner.login + $repo = $gitHubEvent.repository.name + $gh_sender = $gitHubEvent.sender.login # sender is an automatic variable in Powershell + Write-Verbose "Enterprise: $enterprise" + Write-Verbose "Organization: $organization" + Write-Verbose "Repository: $repo" + Write-Verbose "Repository Owner: $owner" + Write-Verbose "Repository Owner Type: $targetType" + Write-Verbose "Sender: $gh_sender" + if ([string]::IsNullOrEmpty($contextObj['Enterprise'])) { + $contextObj['Enterprise'] = [string]$enterprise + } + if ([string]::IsNullOrEmpty($contextObj['Owner'])) { + $contextObj['Owner'] = [string]$owner + } + if ([string]::IsNullOrEmpty($contextObj['Repo'])) { + $contextObj['Repo'] = [string]$repo + } + if ([string]::IsNullOrEmpty($contextObj['TargetType'])) { + $contextObj['TargetType'] = [string]$targetType + } + if ([string]::IsNullOrEmpty($contextObj['TargetName'])) { + $contextObj['TargetName'] = [string]$targetName + } + $contextObj['Name'] = "$($contextObj['HostName'])/$($contextObj['Username'])/" + + "$($contextObj['TargetType'])/$($contextObj['TargetName'])" + } else { + $contextObj['Name'] = "$($contextObj['HostName'])/$($contextObj['Username'])/" + + "$($contextObj['TargetType'])/$($contextObj['TargetName'])" } } 'App' { - $app = Get-GitHubApp -Context $Context - $ContextName = "$($Context['HostName'])/$($app.slug)" - $Context += @{ - Name = $ContextName - DisplayName = [string]$app.name - Username = [string]$app.slug - NodeID = [string]$app.node_id - DatabaseID = [string]$app.id - Permissions = [string]$app.permissions - Events = [string]$app.events - OwnerName = [string]$app.owner.login - OwnerType = [string]$app.owner.type - Type = 'App' - } + $app = Get-GitHubApp -Context $contextObj + $contextObj['Name'] = "$($contextObj['HostName'])/$($app.slug)" + $contextObj['DisplayName'] = [string]$app.name + $contextObj['Username'] = [string]$app.slug + $contextObj['NodeID'] = [string]$app.node_id + $contextObj['DatabaseID'] = [string]$app.id + $contextObj['Permissions'] = [PSCustomObject]$app.permissions + $contextObj['Events'] = [string[]]$app.events + $contextObj['OwnerName'] = [string]$app.owner.login + $contextObj['OwnerType'] = [string]$app.owner.type + $contextObj['Type'] = 'App' } default { throw 'Failed to get info on the context. Unknown logon type.' } } - Write-Verbose "Found [$($Context['Type'])] with login: [$($Context['Name'])]" - $Context | Out-String -Stream | ForEach-Object { Write-Verbose $_ } + Write-Verbose "Found [$($contextObj['Type'])] with login: [$($contextObj['Name'])]" + $contextObj | Out-String -Stream | ForEach-Object { Write-Verbose $_ } Write-Verbose '----------------------------------------------------' if ($PSCmdlet.ShouldProcess('Context', 'Set')) { - Write-Verbose "Saving context: [$($script:GitHub.Config.ID)/$($Context['Name'])]" - Set-Context -ID "$($script:GitHub.Config.ID)/$($Context['Name'])" -Context $Context + Write-Verbose "Saving context: [$($script:GitHub.Config.ID)/$($contextObj['Name'])]" + Set-Context -ID "$($script:GitHub.Config.ID)/$($contextObj['Name'])" -Context $contextObj if ($Default) { - Set-GitHubDefaultContext -Context $Context['Name'] - if ($Context['AuthType'] -eq 'IAT' -and $script:GitHub.EnvironmentType -eq 'GHA') { - Set-GitHubGitConfig -Context $Context['Name'] + Set-GitHubDefaultContext -Context $contextObj['Name'] + if ($contextObj['AuthType'] -eq 'IAT' -and $script:GitHub.EnvironmentType -eq 'GHA') { + Set-GitHubGitConfig -Context $contextObj['Name'] } } if ($PassThru) { - Get-GitHubContext -Context $($Context['Name']) + Get-GitHubContext -Context $($contextObj['Name']) } } } catch { Write-Error $_ | Select-Object * throw 'Failed to set the GitHub context.' + } finally { + $contextObj.Clear() } } diff --git a/tests/GitHub.Tests.ps1 b/tests/GitHub.Tests.ps1 index 8faeacb6a..7916d4b19 100644 --- a/tests/GitHub.Tests.ps1 +++ b/tests/GitHub.Tests.ps1 @@ -50,22 +50,35 @@ Describe 'GitHub' { } It 'Can be called with a GitHub App' { - { Connect-GitHubAccount -ClientID $env:TEST_APP_CLIENT_ID -PrivateKey $env:TEST_APP_PRIVATE_KEY } | Should -Not -Throw - { Connect-GitHubAccount -ClientID $env:TEST_APP_CLIENT_ID -PrivateKey $env:TEST_APP_PRIVATE_KEY } | Should -Not -Throw - Write-Verbose (Get-GitHubContext | Out-String) -Verbose - } - - It 'Can list all contexts' { - Write-Verbose (Get-GitHubContext -ListAvailable | Out-String) -Verbose - (Get-GitHubContext -ListAvailable).Count | Should -Be 7 + $params = @{ + ClientID = $env:TEST_APP_CLIENT_ID + PrivateKey = $env:TEST_APP_PRIVATE_KEY + } + { Connect-GitHubAccount @params } | Should -Not -Throw + $contexts = Get-GitHubContext -ListAvailable -Verbose:$false + Write-Verbose ($contexts | Out-String) -Verbose + ($contexts).Count | Should -Be 3 + } + It 'Can be called with a GitHub App and autoload installations' { + $params = @{ + ClientID = $env:TEST_APP_CLIENT_ID + PrivateKey = $env:TEST_APP_PRIVATE_KEY + } + { Connect-GitHubAccount @params -AutoloadInstallations } | Should -Not -Throw + $contexts = Get-GitHubContext -ListAvailable -Verbose:$false + Write-Verbose ($contexts | Out-String) -Verbose + ($contexts).Count | Should -Be 7 } It 'Can disconnect a specific context' { - { Disconnect-GitHubAccount -Context 'github.com/github-actions/Organization/PSModule' -Silent } | Should -Not -Throw - (Get-GitHubContext -ListAvailable).Count | Should -Be 6 - Connect-GitHubAccount - Connect-GitHubAccount -ClientID $env:TEST_APP_CLIENT_ID -PrivateKey $env:TEST_APP_PRIVATE_KEY - (Get-GitHubContext -ListAvailable).Count | Should -Be 7 + { Disconnect-GitHubAccount -Context 'github.com/psmodule-test-app/Organization/PSModule' -Silent } | Should -Not -Throw + $contexts = Get-GitHubContext -ListAvailable -Verbose:$false + Write-Verbose ($contexts | Out-String) -Verbose + ($contexts).Count | Should -Be 6 + Connect-GitHubAccount -ClientID $env:TEST_APP_CLIENT_ID -PrivateKey $env:TEST_APP_PRIVATE_KEY -AutoloadInstallations + $contexts = Get-GitHubContext -ListAvailable -Verbose:$false + Write-Verbose ($contexts | Out-String) -Verbose + ($contexts).Count | Should -Be 7 } It 'Can get the authenticated GitHubApp' {