Skip to content

Commit bfe8771

Browse files
🪲 [Fix]: Fixes to app installations on enterprise organizations (#437)
## Description This pull request introduces several fixes to improve the handling of GitHub App installations and related functionality. The updates include adding a new constructor for `GitHubAppInstallation`, refining object iteration in private functions, and aligning property names in public functions for consistency. ### Enhancements to `GitHubAppInstallation` class: * Added a new constructor to the `GitHubAppInstallation` class to support initialization with additional parameters, including `Target` and `Type`. This allows for more detailed and flexible object creation. ### Updates to private functions for object iteration: * Replaced `ForEach-Object` with explicit `foreach` loops in `Get-GitHubAppInstallableOrganization`, improving readability and maintainability of the code. * Updated `Get-GitHubEnterpriseOrganizationAppInstallation` to use the new `GitHubAppInstallation` constructor, enabling the inclusion of `Organization` and `Type` parameters for installations. * Refined `Get-GitHubOrganizationAppInstallation` to use `foreach` loops for better clarity and aligned it with the new constructor for `GitHubAppInstallation`. ### Consistency improvements in public functions: * Updated `Connect-GitHubApp` to use the `Target` property instead of `account.slug` for `InstallationName` and `Enterprise`, ensuring consistent property naming. Additionally, fixed a casing issue for the `Name` property. ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [x] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas
1 parent df0e9f5 commit bfe8771

16 files changed

+335
-226
lines changed

src/classes/public/App/GitHubAppInstallation.ps1

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040

4141
GitHubAppInstallation() {}
4242

43-
GitHubAppInstallation([PSCustomObject]$Object) {
43+
GitHubAppInstallation([PSCustomObject] $Object) {
4444
$this.ID = $Object.id
4545
$this.App = [GitHubApp]::new(
4646
[PSCustomObject]@{
@@ -60,4 +60,25 @@
6060
$this.SuspendedAt = $Object.suspended_at
6161
$this.SuspendedBy = [GitHubUser]::new($Object.suspended_by)
6262
}
63+
64+
GitHubAppInstallation([PSCustomObject] $Object, [string] $Target, [string] $Type) {
65+
$this.ID = $Object.id
66+
$this.App = [GitHubApp]::new(
67+
[PSCustomObject]@{
68+
client_id = $Object.client_id
69+
app_id = $Object.app_id
70+
app_slug = $Object.app_slug
71+
}
72+
)
73+
$this.Target = [GitHubOwner]::new($Target)
74+
$this.Type = $Type
75+
$this.RepositorySelection = $Object.repository_selection
76+
$this.Permissions = $Object.permissions
77+
$this.Events = , ($Object.events)
78+
$this.FilePaths = $Object.single_file_paths
79+
$this.CreatedAt = $Object.created_at
80+
$this.UpdatedAt = $Object.updated_at
81+
$this.SuspendedAt = $Object.suspended_at
82+
$this.SuspendedBy = [GitHubUser]::new($Object.suspended_by)
83+
}
6384
}

src/classes/public/Context/GitHubContext/InstallationGitHubContext.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[datetime] $TokenExpirationDate
88

99
# The installation ID.
10-
[int] $InstallationID
10+
[uint64] $InstallationID
1111

1212
# The permissions that the app is requesting on the target
1313
[pscustomobject] $Permissions

src/functions/private/Apps/GitHub Apps/Get-GitHubAppInstallableOrganization.ps1

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@
5151
}
5252

5353
Invoke-GitHubAPI @inputObject | ForEach-Object {
54-
$_.Response | ForEach-Object { [GitHubOrganization]::new($_) }
54+
foreach ($organization in $_.Response) {
55+
[GitHubOrganization]::new($organization)
56+
}
5557
}
5658
}
5759

src/functions/private/Apps/GitHub Apps/Get-GitHubEnterpriseOrganizationAppInstallation.ps1

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
}
5959

6060
Invoke-GitHubAPI @inputObject | ForEach-Object {
61-
[GitHubAppInstallation]::new($_.Response)
61+
foreach ($installation in $_.Response.installations) {
62+
[GitHubAppInstallation]::new($installation, $Organization, 'Organization')
63+
}
6264
}
6365
}
6466

src/functions/private/Apps/GitHub Apps/Get-GitHubOrganizationAppInstallation.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
}
5151

5252
Invoke-GitHubAPI @inputObject | ForEach-Object {
53-
$_.Response.installations | ForEach-Object {
54-
[GitHubAppInstallation]::new($_)
53+
foreach ($installation in $_.Response.installations) {
54+
[GitHubAppInstallation]::new($installation)
5555
}
5656
}
5757
}

src/functions/public/Apps/GitHub App Installations/Get-GitHubAppInstallation.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
PerPage = $PerPage
5757
Context = $Context
5858
}
59+
Write-Debug "ParamSet: $($PSCmdlet.ParameterSetName)"
5960
switch ($PSCmdlet.ParameterSetName) {
6061
'List installations on an Enterprise' {
6162
$params += @{

src/functions/public/Auth/Connect-GitHubApp.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,16 @@
153153
$contextParams['Owner'] = [string]$installation.Target.Name
154154
}
155155
'Enterprise' {
156-
$contextParams['InstallationName'] = [string]$installation.account.slug
157-
$contextParams['Enterprise'] = [string]$installation.account.slug
156+
$contextParams['InstallationName'] = [string]$installation.Target.Name
157+
$contextParams['Enterprise'] = [string]$installation.Target.Name
158158
}
159159
}
160160
Write-Verbose 'Logging in using a managed installation access token...'
161161
$contextParams | Format-Table | Out-String -Stream | ForEach-Object { Write-Verbose $_ }
162162
$contextObj = [InstallationGitHubContext]::new((Set-GitHubContext -Context $contextParams.Clone() -PassThru -Default:$Default))
163163
$contextObj | Format-List | Out-String -Stream | ForEach-Object { Write-Verbose $_ }
164164
if (-not $Silent) {
165-
$name = $contextObj.name
165+
$name = $contextObj.Name
166166
if ($script:GitHub.EnvironmentType -eq 'GHA') {
167167
$green = $PSStyle.Foreground.Green
168168
$reset = $PSStyle.Reset

tests/Apps.Tests.ps1

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '5.7.1' }
2+
3+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
4+
'PSUseDeclaredVarsMoreThanAssignments', '',
5+
Justification = 'Pester grouping syntax: known issue.'
6+
)]
7+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
8+
'PSAvoidUsingConvertToSecureStringWithPlainText', '',
9+
Justification = 'Used to create a secure string for testing.'
10+
)]
11+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
12+
'PSAvoidUsingWriteHost', '',
13+
Justification = 'Log outputs to GitHub Actions logs.'
14+
)]
15+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
16+
'PSAvoidLongLines', '',
17+
Justification = 'Long test descriptions and skip switches'
18+
)]
19+
[CmdletBinding()]
20+
param()
21+
22+
Describe 'Apps' {
23+
$authCases = . "$PSScriptRoot/Data/AuthCases.ps1"
24+
25+
Context 'As <Type> using <Case> on <Target>' -ForEach $authCases {
26+
BeforeAll {
27+
$context = Connect-GitHubAccount @connectParams -PassThru -Silent
28+
LogGroup 'Context' {
29+
Write-Host ($context | Format-List | Out-String)
30+
}
31+
}
32+
33+
AfterAll {
34+
Get-GitHubContext -ListAvailable | Disconnect-GitHubAccount -Silent
35+
Write-Host ('-' * 60)
36+
}
37+
38+
# Tests for APP goes here
39+
if ($AuthType -eq 'APP') {
40+
Context 'GitHub Apps' {
41+
It 'Get-GitHubApp - Can get app details' {
42+
$app = Get-GitHubApp
43+
LogGroup 'App' {
44+
Write-Host ($app | Format-List | Out-String)
45+
}
46+
$app | Should -Not -BeNullOrEmpty
47+
$app | Should -BeOfType 'GitHubApp'
48+
$app.ID | Should -Not -BeNullOrEmpty
49+
$app.ClientID | Should -Not -BeNullOrEmpty
50+
$app.Slug | Should -Not -BeNullOrEmpty
51+
$app.NodeID | Should -Not -BeNullOrEmpty
52+
$app.Owner | Should -BeOfType 'GitHubOwner'
53+
$app.Name | Should -Not -BeNullOrEmpty
54+
$app.Description | Should -Not -BeNullOrEmpty
55+
$app.ExternalUrl | Should -Not -BeNullOrEmpty
56+
$app.Url | Should -Not -BeNullOrEmpty
57+
$app.CreatedAt | Should -Not -BeNullOrEmpty
58+
$app.UpdatedAt | Should -Not -BeNullOrEmpty
59+
$app.Permissions | Should -BeOfType 'PSCustomObject'
60+
$app.Events | Should -BeOfType 'string'
61+
$app.Installations | Should -Not -BeNullOrEmpty
62+
}
63+
64+
It 'Get-GitHubAppJSONWebToken - Can get a JWT for the app' {
65+
$jwt = Get-GitHubAppJSONWebToken @connectParams
66+
LogGroup 'JWT' {
67+
Write-Host ($jwt | Format-Table | Out-String)
68+
}
69+
$jwt | Should -Not -BeNullOrEmpty
70+
}
71+
72+
It 'Get-GitHubAppInstallationRequest - Can get installation requests' {
73+
$installationRequests = Get-GitHubAppInstallationRequest
74+
LogGroup 'Installation requests' {
75+
Write-Host ($installationRequests | Format-List | Out-String)
76+
}
77+
}
78+
79+
It 'Get-GitHubAppInstallation - Can get app installations' {
80+
$githubApp = Get-GitHubApp
81+
$installations = Get-GitHubAppInstallation
82+
LogGroup 'Installations' {
83+
Write-Host ($installations | Format-List | Out-String)
84+
}
85+
$installations | Should -Not -BeNullOrEmpty
86+
foreach ($installation in $installations) {
87+
$installation | Should -BeOfType 'GitHubAppInstallation'
88+
$installation.ID | Should -Not -BeNullOrEmpty
89+
$installation.App | Should -BeOfType 'GitHubApp'
90+
$installation.App.ClientID | Should -Be $githubApp.ClientID
91+
$installation.App.AppID | Should -Not -BeNullOrEmpty
92+
$installation.App.Slug | Should -Not -BeNullOrEmpty
93+
$installation.Target | Should -BeOfType 'GitHubOwner'
94+
$installation.Target | Should -Not -BeNullOrEmpty
95+
$installation.Type | Should -BeIn @('Enterprise', 'Organization', 'User')
96+
$installation.RepositorySelection | Should -Not -BeNullOrEmpty
97+
$installation.Permissions | Should -BeOfType 'PSCustomObject'
98+
$installation.Events | Should -BeOfType 'string'
99+
$installation.CreatedAt | Should -Not -BeNullOrEmpty
100+
$installation.UpdatedAt | Should -Not -BeNullOrEmpty
101+
$installation.SuspendedAt | Should -BeNullOrEmpty
102+
$installation.SuspendedBy | Should -BeOfType 'GitHubUser'
103+
$installation.SuspendedBy | Should -BeNullOrEmpty
104+
}
105+
}
106+
107+
It 'New-GitHubAppInstallationAccessToken - Can get app installation access tokens' {
108+
$installations = Get-GitHubAppInstallation
109+
LogGroup 'Tokens' {
110+
$installations | ForEach-Object {
111+
$token = New-GitHubAppInstallationAccessToken -InstallationID $_.id
112+
Write-Host ($token | Format-List | Out-String)
113+
}
114+
$token | Should -Not -BeNullOrEmpty
115+
}
116+
}
117+
118+
It 'Get-GitHubAppInstallation - <ownerType>' {
119+
$githubApp = Get-GitHubApp
120+
$installation = Get-GitHubAppInstallation | Where-Object { ($_.Target.Name -eq $owner) -and ($_.Type -eq $ownerType) }
121+
LogGroup "Installation - $ownerType" {
122+
Write-Host ($installation | Format-List | Out-String)
123+
}
124+
$installation | Should -Not -BeNullOrEmpty
125+
$installation | Should -BeOfType 'GitHubAppInstallation'
126+
$installation.ID | Should -Not -BeNullOrEmpty
127+
$installation.App | Should -BeOfType 'GitHubApp'
128+
$installation.App.ClientID | Should -Be $githubApp.ClientID
129+
$installation.App.AppID | Should -Not -BeNullOrEmpty
130+
$installation.App.Slug | Should -Not -BeNullOrEmpty
131+
$installation.Target | Should -BeOfType 'GitHubOwner'
132+
$installation.Target | Should -Be $owner
133+
$installation.Type | Should -Be $ownerType
134+
$installation.RepositorySelection | Should -Not -BeNullOrEmpty
135+
$installation.Permissions | Should -BeOfType 'PSCustomObject'
136+
$installation.Events | Should -BeOfType 'string'
137+
$installation.CreatedAt | Should -Not -BeNullOrEmpty
138+
$installation.UpdatedAt | Should -Not -BeNullOrEmpty
139+
$installation.SuspendedAt | Should -BeNullOrEmpty
140+
$installation.SuspendedBy | Should -BeOfType 'GitHubUser'
141+
$installation.SuspendedBy | Should -BeNullOrEmpty
142+
}
143+
}
144+
145+
Context 'Webhooks' {
146+
It 'Get-GitHubAppWebhookConfiguration - Can get the webhook configuration' {
147+
$webhookConfig = Get-GitHubAppWebhookConfiguration
148+
LogGroup 'Webhook config' {
149+
Write-Host ($webhookConfig | Format-Table | Out-String)
150+
}
151+
$webhookConfig | Should -Not -BeNullOrEmpty
152+
}
153+
154+
It 'Update-GitHubAppWebhookConfiguration - Can update the webhook configuration' {
155+
{ Update-GitHubAppWebhookConfiguration -ContentType 'form' } | Should -Not -Throw
156+
$webhookConfig = Get-GitHubAppWebhookConfiguration
157+
LogGroup 'Webhook config - form' {
158+
Write-Host ($webhookConfig | Format-Table | Out-String)
159+
}
160+
{ Update-GitHubAppWebhookConfiguration -ContentType 'json' } | Should -Not -Throw
161+
$webhookConfig = Get-GitHubAppWebhookConfiguration
162+
LogGroup 'Webhook config - json' {
163+
Write-Host ($webhookConfig | Format-Table | Out-String)
164+
}
165+
}
166+
167+
It 'Get-GitHubAppWebhookDelivery - Can get webhook deliveries' {
168+
$deliveries = Get-GitHubAppWebhookDelivery
169+
LogGroup 'Deliveries' {
170+
Write-Host ($deliveries | Format-Table | Out-String)
171+
}
172+
$deliveries | Should -Not -BeNullOrEmpty
173+
}
174+
175+
It 'Get-GitHubAppWebhookDelivery - Can redeliver a webhook delivery' {
176+
$deliveries = Get-GitHubAppWebhookDelivery | Select-Object -First 1
177+
LogGroup 'Delivery - redeliver' {
178+
Write-Host ($deliveries | Format-Table | Out-String)
179+
}
180+
{ Invoke-GitHubAppWebhookReDelivery -ID $deliveries.id } | Should -Not -Throw
181+
LogGroup 'Delivery - redeliver' {
182+
Write-Host ($deliveries | Format-Table | Out-String)
183+
}
184+
}
185+
}
186+
It 'Connect-GitHubApp - Connects as a GitHub App to <Owner>' {
187+
$githubApp = Get-GitHubApp
188+
$config = Get-GitHubConfig
189+
$context = Connect-GitHubApp @connectAppParams -PassThru -Silent
190+
LogGroup 'Context' {
191+
Write-Host ($context | Format-List | Out-String)
192+
}
193+
$context | Should -BeOfType 'InstallationGitHubContext'
194+
$context.ClientID | Should -Be $githubApp.ClientID
195+
$context.TokenExpirationDate | Should -BeOfType [datetime]
196+
$context.InstallationID | Should -BeOfType [uint64]
197+
$context.InstallationID | Should -BeGreaterThan 0
198+
$context.Permissions | Should -BeOfType [PSCustomObject]
199+
$context.Events | Should -BeOfType 'string'
200+
$context.InstallationType | Should -Be $ownertype
201+
$context.InstallationName | Should -Be $owner
202+
$context.ID | Should -Be "$($config.HostName)/$($githubApp.Slug)/$ownertype/$owner"
203+
$context.Name | Should -Be "$($config.HostName)/$($githubApp.Slug)/$ownertype/$owner"
204+
$context.DisplayName | Should -Be $githubApp.Name
205+
$context.Type | Should -Be 'Installation'
206+
$context.HostName | Should -Be $config.HostName
207+
$context.ApiBaseUri | Should -Be $config.ApiBaseUri
208+
$context.ApiVersion | Should -Be $config.ApiVersion
209+
$context.AuthType | Should -Be 'IAT'
210+
$context.NodeID | Should -Not -BeNullOrEmpty
211+
$context.DatabaseID | Should -Not -BeNullOrEmpty
212+
$context.UserName | Should -Be $githubApp.Slug
213+
$context.Token | Should -BeOfType [System.Security.SecureString]
214+
$context.TokenType | Should -Be 'ghs'
215+
$context.HttpVersion | Should -Be $config.HttpVersion
216+
$context.PerPage | Should -Be $config.PerPage
217+
}
218+
}
219+
220+
# Tests for IAT UAT and PAT goes here
221+
It 'Get-GitHubApp - Get an app by slug' -Skip:($AuthType -eq 'APP') {
222+
$app = Get-GitHubApp -Slug 'github-actions'
223+
LogGroup 'App by slug' {
224+
Write-Host ($app | Format-List | Out-String)
225+
}
226+
$app | Should -Not -BeNullOrEmpty
227+
}
228+
}
229+
}

tests/Data/AuthCases.ps1

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,6 @@
4848
Token = $env:GITHUB_TOKEN
4949
}
5050
}
51-
@{
52-
AuthType = 'App'
53-
Type = 'a GitHub App from an Enterprise'
54-
Case = 'PEM + IAT'
55-
TokenType = 'APP_ENT'
56-
Target = 'organization account'
57-
Owner = 'psmodule-test-org3'
58-
OwnerType = 'organization'
59-
ConnectParams = @{
60-
ClientID = $env:TEST_APP_ENT_CLIENT_ID
61-
PrivateKey = $env:TEST_APP_ENT_PRIVATE_KEY
62-
}
63-
ConnectAppParams = @{
64-
Organization = 'psmodule-test-org3'
65-
}
66-
}
6751
@{
6852
AuthType = 'App'
6953
Type = 'a GitHub App from an Organization'
@@ -81,3 +65,35 @@
8165
}
8266
}
8367
)
68+
@{
69+
AuthType = 'App'
70+
Type = 'a GitHub App from an Enterprise'
71+
Case = 'PEM + IAT'
72+
TokenType = 'APP_ENT'
73+
Target = 'organization account'
74+
Owner = 'psmodule-test-org3'
75+
OwnerType = 'organization'
76+
ConnectParams = @{
77+
ClientID = $env:TEST_APP_ENT_CLIENT_ID
78+
PrivateKey = $env:TEST_APP_ENT_PRIVATE_KEY
79+
}
80+
ConnectAppParams = @{
81+
Organization = 'psmodule-test-org3'
82+
}
83+
}
84+
@{
85+
AuthType = 'App'
86+
Type = 'a GitHub App from an Enterprise'
87+
Case = 'PEM + IAT'
88+
TokenType = 'APP_ENT'
89+
Target = 'enterprise account'
90+
Owner = 'msx'
91+
OwnerType = 'enterprise'
92+
ConnectParams = @{
93+
ClientID = $env:TEST_APP_ENT_CLIENT_ID
94+
PrivateKey = $env:TEST_APP_ENT_PRIVATE_KEY
95+
}
96+
ConnectAppParams = @{
97+
Enterprise = 'msx'
98+
}
99+
}

0 commit comments

Comments
 (0)