diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go index 93b0cd1f0..56fd12146 100644 --- a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher.go @@ -63,6 +63,13 @@ const ( apiDotGithubDotCom = "api.github.com" githubDotCom = "github.com" + + headerNameAPIVersion = "X-GitHub-Api-Version" + headerValueAPIVersion2022 = "2022-11-28" + headerNameUserAgent = "User-Agent" + headerValueUserAgentConcierge = "pinniped-concierge/v1" + headerNameAccept = "Accept" + headerValueAcceptGitHubJSON = "application/vnd.github+json" ) // UpstreamGitHubIdentityProviderICache is a thread safe cache that holds a list of validated upstream GitHub IDP configurations. @@ -515,7 +522,7 @@ func (c *gitHubWatcherController) validateGitHubConnection( } // ProbeURL is the production code for how to probe a GitHub URL to test our connection to the GitHub API. -// It can be replaced via constructor injection for testing. +// It can be replaced via constructor injection for testing. Implements ProbeURLFunc. func ProbeURL(ctx context.Context, client *http.Client, url string) error { probeRequestCtx, probeRequestCancel := context.WithTimeout(ctx, 30*time.Second) defer probeRequestCancel() @@ -526,6 +533,12 @@ func ProbeURL(ctx context.Context, client *http.Client, url string) error { return err } + // GitHub API docs say that these headers are required for all requests. + // See https://docs.github.com/en/rest/using-the-rest-api/getting-started-with-the-rest-api?apiVersion=2022-11-28#headers. + probeRequest.Header.Set(headerNameAPIVersion, headerValueAPIVersion2022) + probeRequest.Header.Set(headerNameUserAgent, headerValueUserAgentConcierge) + probeRequest.Header.Set(headerNameAccept, headerValueAcceptGitHubJSON) + probeResponse, err := client.Do(probeRequest) if err != nil { return err diff --git a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go index c56f30c91..51b273dd0 100644 --- a/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/githubupstreamwatcher/github_upstream_watcher_test.go @@ -81,6 +81,34 @@ func fakeGithubProbeFuncDisallowsAllProbes(t *testing.T) ProbeURLFunc { } } +// TestProbeURL tests the production version of the ProbeURLFunc type. +func TestProbeURL(t *testing.T) { + serverEndpointReached := false + testServer, testServerCA := tlsserver.TestServerIPv4(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, []string{"2022-11-28"}, r.Header.Values("X-GitHub-Api-Version")) + require.Equal(t, []string{"pinniped-concierge/v1"}, r.Header.Values("User-Agent")) + require.Equal(t, []string{"application/vnd.github+json"}, r.Header.Values("Accept")) + serverEndpointReached = true + }), nil) + + testServerCertPool := x509.NewCertPool() + testServerCertPool.AppendCertsFromPEM(testServerCA) + httpClient := phttp.Default(testServerCertPool) + + // Happy path. + err := ProbeURL(context.Background(), httpClient, testServer.URL) + require.NoError(t, err) + require.True(t, serverEndpointReached) + + // Invalid URL. + err = ProbeURL(context.Background(), httpClient, "https://invalid hostname") + require.EqualError(t, err, `parse "https://invalid hostname": invalid character " " in host name`) + + // Did not trust server's CA, as an example of a failed connection. + err = ProbeURL(context.Background(), phttp.Default(nil), testServer.URL) + require.EqualError(t, err, fmt.Sprintf("Get %q: tls: failed to verify certificate: x509: certificate signed by unknown authority", testServer.URL)) +} + func TestController(t *testing.T) { require.Equal(t, 6, countExpectedConditions)