auth: route RepoScopedToken through cluster discovery (COR-395) · Entire
auth: route RepoScopedToken through cluster discovery (COR-395)
821be3e→main·
toothbrush·1mo ago·8 files·+190 added/-128 removed
RepoScopedToken minted repo-scoped tokens by exchanging the active context's login JWT at the hard-coded api.AuthBaseURL() core, never asking which core the target cluster trusts. Multi-core this ships the token to the wrong core's STS (exchange fails, or worse authenticates the wrong identity); the same gap COR-389, PR #1377, and git-remote-entire each fixed for their surface.
Resolve the login context the way git-remote-entire does — the cluster's /.well-known/entire-cluster.json names its trusted core(s), clusterdiscovery picks the eligible local context (active if eligible, sole, or explicit-choice error) — then exchange through repocreds with a refreshing login provider. This also closes the documented stale login-JWT gap (expired JWTs now re-mint from the refresh token) and makes the wire form exactly what git-remote-entire sends, client_id via Basic auth included.
httputil.OAuthError gains a Code field (RFC 6749 error, parsed best-effort) so the suspended-mirror sentinel ErrRepoTargetUnknown keeps working over the repocreds path.
The second COR-395 item, deleting resolveAuthHostToken, was already done; this removes the last stale comment reference.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Sessions
Changes
8
cmd/entire/cli
auth
Mdata_api.go+6/-6
Mdata_api_test.go+1/-1
Mrepo_token.go+66/-83
Mrepo_token_test.go+78/-34
Mrepo_mirror.go+1/-1
Mrepo_mirror_probe.go+1/-1
internal/entireclient/httputil
Moauth.go+8/-2
Moauth_test.go+29
// resolution, so a slow or absent endpoint must not stall the command.
const dataAPIDiscoveryTimeout = 8 * time.Second
// resolveContextForAPIFunc is the shape of the discovery seam: it mirrors
// clusterdiscovery.ResolveContextForAPI (ctx, configDir, cacheDir, apiHost,
// httpClient, debugf).
type resolveContextForAPIFunc func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error)
// resolveContextFunc is the shape of a context-discovery seam: it mirrors
// clusterdiscovery.ResolveContextForAPI / ResolveContextForCluster
// (ctx, configDir, cacheDir, host, httpClient, debugf).
type resolveContextFunc func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error)
// resolveContextForAPI is the discovery seam, swapped in tests so they don't
// reach the network. See SetResolveContextForAPIForTest for cross-package tests.
var resolveContextForAPI resolveContextForAPIFunc = clusterdiscovery.ResolveContextForAPI
var resolveContextForAPI resolveContextFunc = clusterdiscovery.ResolveContextForAPI
// SetResolveContextForAPIForTest overrides the /.well-known/entire-api.json
// discovery seam and returns a cleanup func. Tests in other packages that
// configured data host and bypasses any SetManagerForTest fallback seam. Pass
// a func returning clusterdiscovery.ErrDiscoveryUnavailable to force the static
// fallback path. Test-only.
func SetResolveContextForAPIForTest(t interface{ Helper() }, fn resolveContextForAPIFunc) func() {
func SetResolveContextForAPIForTest(t interface{ Helper() }, fn resolveContextFunc) func() {
t.Helper()
prev := resolveContextForAPI
resolveContextForAPI = fn
}
func RepoScopedToken(ctx context.Context, clusterBaseURL, repoSlug, action string) (string, error) {
provider := CurrentProvider()
if strings.TrimSpace(provider.STSPath) == "" {
return "", errors.New("repo-scoped token exchange requires a v2 auth host (set ENTIRE_AUTH_BASE_URL to a login server that exposes /oauth/token)")
}
loginJWT, err := LookupCurrentToken()
clusterCtx, err := resolveContextForCluster(ctx, contexts.DefaultConfigDir(), discovery.DefaultCacheDir(), clusterHost, httpClient, nil)
if err != nil {
return "", fmt.Errorf("read login token: %w", err)
}
if loginJWT == "" {
return "", ErrNotLoggedIn
}
// Exchange logic here
}
ErrRepoTargetUnknown reports that the cluster's STS refused the exchangerepoExchangeTransportForTest, when non-nil, is used as the sts.ClientTestRepoScopedToken_InvalidTarget asserts that a 400 invalid_target STS
Additional Information
The above content includes various comments and code snippets that pertain to the implementation and testing of the RepoScopedToken function. The code contains significant detail about error handling, testing scenarios, and the overall workings of the token exchange process defined in the given package.