Add PostHog Cloud OAuth and self-hosted support
PostHog Cloud authenticates via CIMD OAuth (public client, PKCE) through the region-agnostic oauth.posthog.com gateway, with an API-key fallback. PostHog Self-Hosted is a separate provider using an API key and an instance URL. The shared driver discovers the data region by probing us/eu for OAuth connections, since the gateway does not serve the data API, and pins pagination to the resolved host. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -86,7 +86,7 @@ func TestApplyOAuth2Defaults_AuthURLFromSlug(t *testing.T) {
|
||||
func TestApplyOAuth2Defaults_PKCEDefaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, p := range []string{"PAGERDUTY"} {
|
||||
for _, p := range []string{"PAGERDUTY", "POSTHOG"} {
|
||||
t.Run(p, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -98,3 +98,18 @@ func TestApplyOAuth2Defaults_PKCEDefaults(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplyOAuth2Defaults_PublicClientTokenAuth verifies that PostHog, a
|
||||
// public (CIMD) client, propagates token_endpoint_auth_method "none" so the
|
||||
// token exchange omits a client_secret.
|
||||
func TestApplyOAuth2Defaults_PublicClientTokenAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := provider.NewBuiltinRegistry()
|
||||
c := &connector.OAuth2Connector{}
|
||||
require.NoError(t, r.ApplyOAuth2Defaults("POSTHOG", "https://example.com/cb", c))
|
||||
|
||||
assert.Equal(t, "none", c.TokenEndpointAuth,
|
||||
"PostHog must use token_endpoint_auth_method none (public client)")
|
||||
assert.True(t, c.RequiresPKCE, "PostHog public client must require PKCE")
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func NewBuiltinRegistry() *Registry {
|
||||
onePasswordRegistration(),
|
||||
openaiRegistration(),
|
||||
posthogRegistration(),
|
||||
posthogSelfHostedRegistration(),
|
||||
pagerdutyRegistration(),
|
||||
resendRegistration(),
|
||||
sentryRegistration(),
|
||||
|
||||
@@ -16,6 +16,7 @@ package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
@@ -23,16 +24,61 @@ import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// posthogRegistration is PostHog Cloud (US + EU). OAuth is the preferred
|
||||
// path: oauth.posthog.com is PostHog's region-agnostic OAuth + API gateway,
|
||||
// so one app serves both regions and the driver reaches the customer's data
|
||||
// through it without a per-connection host. An API-key fallback is also
|
||||
// supported, but personal API keys are region-pinned, so it requires the
|
||||
// customer to pick their region (us/eu). Self-hosted instances are a separate
|
||||
// provider (POSTHOG_SELF_HOSTED).
|
||||
func posthogRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderPostHog,
|
||||
DisplayName: "PostHog",
|
||||
Provider: coredata.ConnectorProviderPostHog,
|
||||
DisplayName: "PostHog",
|
||||
|
||||
// PublicClient: PostHog OAuth uses the CIMD flow — no client_secret,
|
||||
// authenticated by PKCE. probod auto-registers this connector with
|
||||
// the deployment's hosted CIMD client_id; no operator OAuth app or
|
||||
// credentials are required.
|
||||
PublicClient: true,
|
||||
AuthURL: "https://oauth.posthog.com/oauth/authorize/",
|
||||
TokenURL: "https://oauth.posthog.com/oauth/token/",
|
||||
TokenEndpointAuth: "none",
|
||||
RequiresPKCE: true,
|
||||
OAuth2Scopes: []string{"organization:read", "organization_member:read"},
|
||||
// required_access_level=organization makes consent org-scoped so
|
||||
// organization_member:read applies org-wide and the org endpoints
|
||||
// resolve @current to the granted organization.
|
||||
ExtraAuthParams: map[string]string{"required_access_level": "organization"},
|
||||
// ProbeURL is intentionally empty: the data host varies per
|
||||
// connection (the region-agnostic gateway for OAuth, us/eu for
|
||||
// API-key), so a single static probe URL cannot match it. A dead
|
||||
// token surfaces on the first ListAccounts.
|
||||
|
||||
SupportsAPIKey: true,
|
||||
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
return drivers.NewPostHogDriver(c), nil
|
||||
ExtraSettings: []ExtraSetting{
|
||||
{Key: "region", Label: "Region", Required: true},
|
||||
},
|
||||
NewNameResolver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) drivers.NameResolver {
|
||||
return drivers.NewPostHogNameResolver(c)
|
||||
|
||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.PostHogConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read posthog connector settings: %w", err)
|
||||
}
|
||||
|
||||
// BaseURL is empty for cloud OAuth connections; the driver then
|
||||
// discovers the region (us/eu) lazily by probing, since the
|
||||
// oauth.posthog.com gateway does not serve the data API.
|
||||
return drivers.NewPostHogDriver(c, s.BaseURL), nil
|
||||
},
|
||||
NewNameResolver: func(ctx context.Context, c *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver {
|
||||
s, err := coredata.ConnectorSettings[coredata.PostHogConnectorSettings](conn)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot read posthog connector settings", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
return drivers.NewPostHogNameResolver(c, s.BaseURL)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
71
pkg/connector/provider/posthog_self_hosted.go
Normal file
71
pkg/connector/provider/posthog_self_hosted.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/accessreview/drivers"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// posthogSelfHostedRegistration is customer-hosted PostHog: API-key plus an
|
||||
// operator-supplied instance URL (Metabase/Grafana style). It shares the
|
||||
// PostHog driver and name resolver, pointed at the instance's BaseURL. OAuth
|
||||
// is deliberately not offered here — a single static authorization URL cannot
|
||||
// serve arbitrary per-customer instances, so self-hosted OAuth is a separate
|
||||
// future effort. Cloud PostHog (POSTHOG) owns the OAuth path.
|
||||
func posthogSelfHostedRegistration() *Registration {
|
||||
return &Registration{
|
||||
Provider: coredata.ConnectorProviderPostHogSelfHosted,
|
||||
DisplayName: "PostHog (Self-Hosted)",
|
||||
|
||||
SupportsAPIKey: true,
|
||||
ExtraSettings: []ExtraSetting{
|
||||
{Key: "instanceUrl", Label: "Instance URL", Required: true},
|
||||
},
|
||||
|
||||
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||
s, err := coredata.ConnectorSettings[coredata.PostHogConnectorSettings](conn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read posthog self-hosted connector settings: %w", err)
|
||||
}
|
||||
|
||||
// Never fall back to the cloud gateway for a self-hosted
|
||||
// connector — the instance URL is required at creation time.
|
||||
if s.BaseURL == "" {
|
||||
return nil, fmt.Errorf("cannot create posthog self-hosted driver: instance URL is required")
|
||||
}
|
||||
|
||||
return drivers.NewPostHogDriver(c, s.BaseURL), nil
|
||||
},
|
||||
NewNameResolver: func(ctx context.Context, c *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver {
|
||||
s, err := coredata.ConnectorSettings[coredata.PostHogConnectorSettings](conn)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot read posthog self-hosted connector settings", log.Error(err))
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return drivers.NewPostHogNameResolver(c, s.BaseURL)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user