Files
probo/pkg/connector/provider/google_analytics.go
Aurélien Sibiril 728011c042 Harden the Google Analytics account fetch
Three defects, all found reviewing the rebased branch.

The connection probe hit /v1alpha/accounts, which any analytics.readonly
grant can call, while the driver's first request is the account's
accessBindings — that additionally needs Administrator on the account
and the manage.users.readonly scope. An Editor connecting, or a user
declining the second scope on Google's granular consent screen, probed
green and then 403'd on every campaign fetch, leaving the source
permanently "Connected" with no rows. The probe now targets the same
accessBindings collection the driver reads.

A single unreadable property aborted the whole account. A property the
token cannot see, or one deleted between the list and the read, threw
away every binding already collected; 49 of 50 readable properties are
still worth reviewing, so 403 and 404 now skip that property. Anything
else still fails the fetch.

Fan-out errors named no resource: the account call, the property list
and each per-property call all returned the same "unexpected status"
string, so a 403 on one subproperty out of forty was unattributable.
Errors now carry the account or property ID.

The cassette gains a subproperty parented to another property (only
reachable through the ancestor filter, so it pins the hierarchy walk
that the filter change claimed) and a property returning 403.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-07-26 09:22:05 +02:00

86 lines
3.6 KiB
Go

// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// 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"
)
func googleAnalyticsRegistration() *Registration {
return &Registration{
Provider: coredata.ConnectorProviderGoogleAnalytics,
DisplayName: "Google Analytics",
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth",
TokenURL: "https://oauth2.googleapis.com/token",
ExtraAuthParams: map[string]string{
"access_type": "offline",
"prompt": "consent",
},
SupportsIncrementalAuth: true,
// analytics.readonly is required to LIST accounts and properties (the
// picker and the probe); analytics.manage.users.readonly is required to
// read the access bindings. The manage.users scope alone cannot list
// accounts (it returns 403), so both are requested.
OAuth2Scopes: []string{
"https://www.googleapis.com/auth/analytics.readonly",
"https://www.googleapis.com/auth/analytics.manage.users.readonly",
},
// BuildProbeURL targets the selected account's accessBindings rather
// than the accounts list: listing accounts only needs
// analytics.readonly, so a non-Administrator connection (or one where
// the user declined manage.users.readonly on Google's granular consent
// screen) would probe green and then 403 on every fetch.
BuildProbeURL: buildGoogleAnalyticsProbeURL,
NewDriver: func(_ context.Context, c *http.Client, conn *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
s, err := coredata.ConnectorSettings[coredata.GoogleAnalyticsConnectorSettings](conn)
if err != nil {
return nil, fmt.Errorf("cannot read google analytics connector settings: %w", err)
}
if s.AccountID == "" {
return nil, fmt.Errorf("cannot create google analytics driver: account_id is required")
}
return drivers.NewGoogleAnalyticsDriver(c, s.AccountID), nil
},
NewNameResolver: func(ctx context.Context, c *http.Client, conn *coredata.Connector, logger *log.Logger) drivers.NameResolver {
s, err := coredata.ConnectorSettings[coredata.GoogleAnalyticsConnectorSettings](conn)
if err != nil {
logger.ErrorCtx(ctx, "cannot read google analytics connector settings", log.Error(err))
return nil
}
return drivers.NewGoogleAnalyticsNameResolver(c, s.AccountID)
},
SetOrganizationSettings: func(c *coredata.Connector, accountID string) error {
return c.SetSettings(&coredata.GoogleAnalyticsConnectorSettings{AccountID: accountID})
},
}
}