Add ExtraAudience option to RoundTripper to allow operators to include additional audiences (e.g., provisioning group) when connecting to the multitenant aggregator. This ensures tokens include both the target API server's audience and the provisioning group audience, which is required to pass the enforceManagerProperties check. - Add ExtraAudience RoundTripperOption - Improve documentation and comments - Add comprehensive test coverage
87 lines
3.1 KiB
Go
87 lines
3.1 KiB
Go
// Package auth provides authentication utilities for the provisioning API.
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/grafana/authlib/authn"
|
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
|
)
|
|
|
|
// tokenExchanger abstracts the token exchange client for testability.
|
|
type tokenExchanger interface {
|
|
Exchange(ctx context.Context, req authn.TokenExchangeRequest) (*authn.TokenExchangeResponse, error)
|
|
}
|
|
|
|
// RoundTripperOption configures optional behavior for the RoundTripper.
|
|
type RoundTripperOption func(*RoundTripper)
|
|
|
|
// ExtraAudience appends an additional audience to the token exchange request.
|
|
//
|
|
// This is primarily used by operators connecting to the multitenant aggregator,
|
|
// where the token must include both the target API server's audience (e.g., dashboards,
|
|
// folders) and the provisioning group audience. The provisioning group audience is
|
|
// required so that the token passes the enforceManagerProperties check, which prevents
|
|
// unauthorized updates to provisioned resources.
|
|
//
|
|
// Example:
|
|
//
|
|
// authrt.NewRoundTripper(client, rt, "dashboards.grafana.app", authrt.ExtraAudience("provisioning.grafana.app"))
|
|
func ExtraAudience(audience string) RoundTripperOption {
|
|
return func(rt *RoundTripper) {
|
|
rt.extraAudience = audience
|
|
}
|
|
}
|
|
|
|
// RoundTripper is an http.RoundTripper that performs token exchange before each request.
|
|
// It exchanges the service's credentials for an access token scoped to the configured
|
|
// audience(s), then injects that token into the outgoing request's X-Access-Token header.
|
|
type RoundTripper struct {
|
|
client tokenExchanger
|
|
transport http.RoundTripper
|
|
audience string
|
|
extraAudience string
|
|
}
|
|
|
|
// NewRoundTripper creates a RoundTripper that exchanges tokens for each outgoing request.
|
|
//
|
|
// Parameters:
|
|
// - tokenExchangeClient: the client used to exchange credentials for access tokens
|
|
// - base: the underlying transport to delegate requests to after token injection
|
|
// - audience: the primary audience for the token (typically the target API server's group)
|
|
// - opts: optional configuration (e.g., ExtraAudience to include additional audiences)
|
|
func NewRoundTripper(tokenExchangeClient tokenExchanger, base http.RoundTripper, audience string, opts ...RoundTripperOption) *RoundTripper {
|
|
rt := &RoundTripper{
|
|
client: tokenExchangeClient,
|
|
transport: base,
|
|
audience: audience,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(rt)
|
|
}
|
|
return rt
|
|
}
|
|
|
|
// RoundTrip exchanges credentials for an access token and injects it into the request.
|
|
// The token is scoped to all configured audiences and the wildcard namespace ("*").
|
|
func (t *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
audiences := []string{t.audience}
|
|
if t.extraAudience != "" && t.extraAudience != t.audience {
|
|
audiences = append(audiences, t.extraAudience)
|
|
}
|
|
|
|
tokenResponse, err := t.client.Exchange(req.Context(), authn.TokenExchangeRequest{
|
|
Audiences: audiences,
|
|
Namespace: "*",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to exchange token: %w", err)
|
|
}
|
|
|
|
req = utilnet.CloneRequest(req)
|
|
req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token)
|
|
return t.transport.RoundTrip(req)
|
|
}
|