auth: discovery-only data-API resolution, delete TokenForResource (COR-393) · Entire

auth: discovery-only data-API resolution, delete TokenForResource (COR-393)

10aa97e→main·

toothbrush·1mo ago·5 files·+57 added/-253 removed

ResolveDataAPIToken loses both static fallbacks: a host that doesn't
advertise /.well-known/entire-api.json (or has no parseable host) is
nnow an error naming the host — without discovery we can't know which
login servers it trusts, and guessing risks exchanging a token at a
core the host doesn't accept. entire.io#2277 shipped the well-known,
so the old-deployment case the fallback existed for is gone.

With its last callers removed, TokenForResource and the process-wide
singleton manager go too, along with the SetManagerForTest /
DiscoveryUnavailableForTest seams and the test scaffolding that only
existed to pin the fallback path.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

7056101a1d83View transcript

Changes

5

3 unmodified lines

4
5
6
7
8
9
10
11
12
11
12
13
14
15
16
17
18
19
8 unmodified lines

28
29
30
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
31
32
33
34
35
36
37
38
39
40
41
42
43
17 unmodified lines

61
62
63
66
67
68
69
70
71
72
73
74
75
76
77
64
65
66
67
68
69
70
71
72
73
74
75
76
77

3 unmodified lines

"bytes"
    "context"
    "errors"
    "net/http"
    "path/filepath"
    "strings"
    "testing"
    "time"

"github.com/entireio/auth-go/sts"
    "github.com/entireio/auth-go/tokens"
    "github.com/entireio/cli/cmd/entire/cli/auth"
    "github.com/entireio/cli/internal/entireclient/clusterdiscovery"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/entireio/cli/internal/entireclient/tokenstore"
}

func strPtr(v string) *string { return &v }

// failures got mis-labeled. This PR surfaces real errors but has to
// keep the cancellation case silent.
func TestRunActivity_SilencesContextCanceled(t *testing.T) {
    // No t.Parallel: SetManagerForTest mutates package-level auth state.
    store := newAuthMemStore()
    saveCoreToken(t, store, authResolveTestIssuer, "opaque-core-token")

mgr := newResolveTestManager(t, store, func(context.Context, sts.ExchangeRequest) (*tokens.TokenSet, error) {
        // Simulate the user hitting Ctrl+C mid-exchange. The real
        // transport would return ctx.Err() wrapped; both shapes flow
        // through errors.Is(err, context.Canceled) identically.
        return nil, context.Canceled
    })

t.Cleanup(auth.SetManagerForTest(t, mgr))

// Force discovery-unavailable so ResolveDataAPIToken takes the static
    // fallback through the singleton test manager above, rather than making a
    // real network fetch to the configured data host.

t.Cleanup(auth.SetResolveContextForAPIForTest(t, auth.DiscoveryUnavailableForTest))
    // No t.Parallel: SetResolveContextForAPIForTest mutates package-level
    // auth state.
    //
    // Simulate the user hitting Ctrl+C during auth resolution: the
    // cancellation surfaces from the discovery fetch, and runActivity must
    // silence it rather than mislabel it "Not logged in".

t.Cleanup(auth.SetResolveContextForAPIForTest(t,
        func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) {
            return nil, context.Canceled
        }))

var out, errOut bytes.Buffer
    err := runActivity(t.Context(), &out, &errOut)
...

...

}

// hint and a SilentError so the raw "not logged in" string doesn't
// also print via cobra.
func TestRunActivity_PrintsLoginHintOnNotLoggedIn(t *testing.T) {
    // No t.Parallel: SetManagerForTest mutates package-level auth state.
    store := newAuthMemStore() // empty: LookupCoreToken returns "" → ErrNotLoggedIn

mgr := newResolveTestManager(t, store, func(context.Context, sts.ExchangeRequest) (*tokens.TokenSet, error) {
        t.Fatal("exchange should not run when no core token is stored")
        return nil, errors.New("unreachable")
    })

t.Cleanup(auth.SetManagerForTest(t, mgr))
    
    // Force discovery-unavailable so ResolveDataAPIToken takes the static
    // fallback through the singleton test manager above, rather than making a
    // real network fetch to the configured data host.

t.Cleanup(auth.SetResolveContextForAPIForTest(t, auth.DiscoveryUnavailableForTest))
    
    c := &contexts.Context{Name: "me@core", CoreURL: "https://core.example", Handle: "me", KeychainService: "kc:me"}

t.Cleanup(auth.SetResolveContextForAPIForTest(t,
        func(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) {
            return c, nil
        }))

var out, errOut bytes.Buffer
    err := runActivity(t.Context(), &out, &errOut)

Mcmd/entire/cli/activity_cmd_test.go+26/-29

2 unmodified lines

3
4
5
6
7
8
9
5 unmodified lines

15
16
17
17
18
18
19
20
21
22
9 unmodified lines

32
33
34
34
35
36
35
36
37
38
1 unmodified line

40
41
42
44
45
46
47
48
49
50
43
44
45
7 unmodified lines

53
54
55
64
65
66
67
68
69
56
57
58
59
60
61
72
73
62
63
64
65
66
67
78
79
68
69
70
82
83
84
85
86
71
72
73
74
75
76
93
94
77
78
79
80

2 unmodified lines

import (
    "context"
    "errors"
    "fmt"
    "net/http"
    "net/url"
    "testing"

"github.com/entireio/auth-go/sts"
    "github.com/entireio/auth-go/tokenmanager"

"github.com/entireio/cli/cmd/entire/cli/api"
    "github.com/entireio/cli/internal/entireclient/clusterdiscovery"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/entireio/cli/internal/entireclient/tokenstore"
)

// dataAPIDiscoveryTimeout bounds the one /.well-known/entire-api.json GET we
// add per data-API command. Kept short: on any failure we fall back to static
// resolution, so a slow or absent endpoint must not stall the command.
const dataAPIDiscoveryTimeout = 8 * time.Second

// resolveContextFunc is the shape of a context-discovery seam: it mirrors
// discovery seam and returns a cleanup func. Tests in other packages that
// exercise a data-API command (activity/search/dispatch/recap) MUST install
// this — otherwise ResolveDataAPIToken makes a real network call to the
// 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 resolveContextFunc) func() {

t.Helper()
    prev := resolveContextForAPI
    resolveContextForAPI = fn
    return func() {
        resolveContextForAPI = prev
    }
}

// DiscoveryUnavailableForTest is a ready-made SetResolveContextForAPIForTest
// value that forces the discovery-unavailable fallback (no network), so a
// cross-package test exercises the static TokenForResource path deterministically.
func DiscoveryUnavailableForTest(context.Context, string, string, string, *http.Client, clusterdiscovery.DebugFunc) (*contexts.Context, error) {
    return nil, clusterdiscovery.ErrDiscoveryUnavailable
}

// ResolveDataAPIToken returns a bearer for the data API at dataBaseURL.
// It dials the API's /.well-known/entire-api.json to learn which login
// authenticate as the partial.to login even while the active context is a
// prod entire.io login — with no per-command override needed.
// A reachable API whose context selection fails (no eligible context, or several with none active)
// surfaces that error directly — the user must log in or pick one.

func ResolveDataAPIToken(ctx context.Context, dataBaseURL string) (string, error) {
    dataOrigin := api.OriginOnly(dataBaseURL)
    host, ok := hostOf(dataOrigin)
    if !ok {
        return TokenForResource(ctx, dataOrigin)
    }

_, _ = MigrateLegacyLoginContext() //nolint:errcheck // best-effort bridge; resolution proceeds regardless
    dctx, cancel := context.WithTimeout(ctx, dataAPIDiscoveryTimeout)
    defer cancel()
    httpClient := &http.Client{Timeout: dataAPIDiscoveryTimeout}

selected, err := resolveContextForAPI(dctx, userdirs.Config(), userdirs.Cache(), host, httpClient, nil)
    if errors.Is(err, clusterdiscovery.ErrDiscoveryUnavailable) {
        return TokenForResource(ctx, dataOrigin)
    }
    if err != nil {
        return "", err
    }

return selected.AccessToken, nil
}
``