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:
@@ -30,7 +30,10 @@ func herokuRegistration() *Registration {
|
|||||||
DisplayName: "Heroku",
|
DisplayName: "Heroku",
|
||||||
AuthURL: "https://id.heroku.com/oauth/authorize",
|
AuthURL: "https://id.heroku.com/oauth/authorize",
|
||||||
TokenURL: "https://id.heroku.com/oauth/token",
|
TokenURL: "https://id.heroku.com/oauth/token",
|
||||||
ProbeURL: "https://api.heroku.com/account",
|
// 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"},
|
OAuth2Scopes: []string{"read"},
|
||||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||||
s, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](conn)
|
s, err := coredata.ConnectorSettings[coredata.HerokuConnectorSettings](conn)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
anthropicAPIVersion = "2023-06-01"
|
anthropicAPIVersion = "2023-06-01"
|
||||||
anthropicUsersProbeURL = "https://api.anthropic.com/v1/organizations/users?limit=1"
|
anthropicUsersProbeURL = "https://api.anthropic.com/v1/organizations/users?limit=1"
|
||||||
|
herokuAccountProbeURL = "https://api.heroku.com/account"
|
||||||
linearGraphQLEndpoint = "https://api.linear.app/graphql"
|
linearGraphQLEndpoint = "https://api.linear.app/graphql"
|
||||||
mondayGraphQLEndpoint = "https://api.monday.com/v2"
|
mondayGraphQLEndpoint = "https://api.monday.com/v2"
|
||||||
posthogOrganizationPath = "/api/organizations/@current/"
|
posthogOrganizationPath = "/api/organizations/@current/"
|
||||||
@@ -410,6 +411,26 @@ func probeAnthropic(
|
|||||||
return doProbeRequest(httpClient, req)
|
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(
|
func probePostHog(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
httpClient *http.Client,
|
httpClient *http.Client,
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -23,6 +25,12 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/coredata"
|
"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) {
|
func TestBuiltinRegistry_ProbeCoverage(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -103,3 +111,47 @@ func TestBuildPostHogProbeURL(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "https://us.posthog.com/api/organizations/@current/", probeURL)
|
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