Fix Heroku connection probe Accept header
Heroku's connection-status probe used a static ProbeURL, which the generic probe issues with `Accept: application/json`. Heroku negotiates the API version through the Accept media type and returns 400 for an unversioned request, which doProbeRequest reads as "connected" -- so the probe never caught a revoked token (it only surfaced at the first ListAccounts). Probe via a probeHeroku closure that sends `Accept: application/vnd.heroku+json; version=3` instead. Verified live: a dead token returns 400 with application/json but 401 with the versioned header, which doProbeRequest correctly maps to rejected. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -26,11 +26,14 @@ import (
|
||||
|
||||
func herokuRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderHeroku,
|
||||
DisplayName: "Heroku",
|
||||
AuthURL: "https://id.heroku.com/oauth/authorize",
|
||||
TokenURL: "https://id.heroku.com/oauth/token",
|
||||
ProbeURL: "https://api.heroku.com/account",
|
||||
Provider: coredata.ConnectorProviderHeroku,
|
||||
DisplayName: "Heroku",
|
||||
AuthURL: "https://id.heroku.com/oauth/authorize",
|
||||
TokenURL: "https://id.heroku.com/oauth/token",
|
||||
// Heroku requires the versioned Accept header; a plain ProbeURL GET
|
||||
// (Accept: application/json) returns 400 and would read as connected,
|
||||
// so probe via a closure that sends application/vnd.heroku+json.
|
||||
Probe: probeHeroku,
|
||||
OAuth2Scopes: []string{"read"},
|
||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](conn)
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
const (
|
||||
anthropicAPIVersion = "2023-06-01"
|
||||
anthropicUsersProbeURL = "https://api.anthropic.com/v1/organizations/users?limit=1"
|
||||
herokuAccountProbeURL = "https://api.heroku.com/account"
|
||||
linearGraphQLEndpoint = "https://api.linear.app/graphql"
|
||||
mondayGraphQLEndpoint = "https://api.monday.com/v2"
|
||||
posthogOrganizationPath = "/api/organizations/@current/"
|
||||
@@ -410,6 +411,26 @@ func probeAnthropic(
|
||||
return doProbeRequest(httpClient, req)
|
||||
}
|
||||
|
||||
func probeHeroku(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
_ *coredata.Connector,
|
||||
) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, herokuAccountProbeURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create probe request: %w", err)
|
||||
}
|
||||
|
||||
// Heroku negotiates the API version through the Accept media type; the
|
||||
// generic "application/json" the default probe sends yields 400 (not
|
||||
// 401/403), which doProbeRequest would read as "connected" and mask a
|
||||
// dead token. Send the versioned Accept so a revoked token surfaces as
|
||||
// 401 (verified live: 400 with application/json, 401 with this header).
|
||||
req.Header.Set("Accept", "application/vnd.heroku+json; version=3")
|
||||
|
||||
return doProbeRequest(httpClient, req)
|
||||
}
|
||||
|
||||
func probePostHog(
|
||||
ctx context.Context,
|
||||
httpClient *http.Client,
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -23,6 +25,12 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// probeRoundTripFunc lets a test capture the probe request and return a
|
||||
// canned response without touching the network.
|
||||
type probeRoundTripFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f probeRoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||
|
||||
func TestBuiltinRegistry_ProbeCoverage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -103,3 +111,47 @@ func TestBuildPostHogProbeURL(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "https://us.posthog.com/api/organizations/@current/", probeURL)
|
||||
}
|
||||
|
||||
func TestProbeHeroku(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// The fix's contract: probeHeroku must send Heroku's versioned Accept
|
||||
// header — a plain "application/json" returns 400, which doProbeRequest
|
||||
// reads as connected and masks a dead token — and it must map 401/403 to
|
||||
// a rejection while letting 2xx pass.
|
||||
cases := []struct {
|
||||
name string
|
||||
status int
|
||||
wantReject bool
|
||||
}{
|
||||
{"valid credential", http.StatusOK, false},
|
||||
{"revoked credential", http.StatusUnauthorized, true},
|
||||
{"forbidden credential", http.StatusForbidden, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var gotAccept, gotURL string
|
||||
|
||||
client := &http.Client{Transport: probeRoundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
gotAccept = r.Header.Get("Accept")
|
||||
gotURL = r.URL.String()
|
||||
|
||||
return &http.Response{StatusCode: tc.status, Body: http.NoBody, Header: make(http.Header)}, nil
|
||||
})}
|
||||
|
||||
err := probeHeroku(context.Background(), client, &coredata.Connector{Provider: coredata.ConnectorProviderHeroku})
|
||||
|
||||
assert.Equal(t, "application/vnd.heroku+json; version=3", gotAccept)
|
||||
assert.Equal(t, "https://api.heroku.com/account", gotURL)
|
||||
|
||||
if tc.wantReject {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user