git-remote-entire: ENTIRE_TOKEN env override for CI / workload identity · Entire

git-remote-entire: ENTIRE_TOKEN env override for CI / workload identity

657a7ed·

toothbrush·1mo ago·3 files·+176 added/-18 removed

Let ENTIRE_TOKEN= bypass contexts.json and the keyring entirely so CI and workload-identity runners clone entire:// URLs without an interactive login.

When set, derive the home-region core URL from the token's URL-shaped aud claim (login/sa-session JWTs carry aud=, which is what STS routing keys on) and use the env token verbatim as the login JWT for repo-scoped exchange. A token with no URL-shaped aud is a hard error, not a silent fallback to context resolution.

tokens.ParseClaims already exposes Audience (normalised across string and array forms), so no helper port was needed.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sessions

2f3a007d8690View transcript

Changes

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

package auth

import (
    "fmt"
    "net/url"
    "strings"

"github.com/entireio/auth-go/tokens"
)

// EnvTokenVar is the environment variable that, when set, bypasses
// contexts.json and the keyring entirely: its value is used verbatim as the
// login JWT for repo-scoped token exchange. This is the CI / workload-identity
// path — a runner injects a short-lived login or sa-session JWT and clones
// without an interactive `entire login`.
const EnvTokenVar = "ENTIRE_TOKEN"

// CoreURLFromEnvToken derives the home-region core URL from an ENTIRE_TOKEN
// JWT's audience claim. Login and sa-session JWTs carry aud=<home-region URL>,
// which is what STS routing keys on — so we read aud, not iss (iss may be a
// regional core that can't mint the cross-region exchange).
//
// The aud claim is URL-shaped (scheme://host[/path]). It may be a single
// string or an array (RFC 7519 §4.1.3); ParseClaims normalises both to a
// slice. The first URL-shaped audience wins. A token with no URL-shaped aud
// is rejected with a clear error rather than silently falling back to context
// resolution, so a misconfigured CI token fails loudly.
func CoreURLFromEnvToken(rawToken string) (string, error) {
    claims, err := tokens.ParseClaims(rawToken)
    if err != nil {
        return "", fmt.Errorf("parse %s claims: %w", EnvTokenVar, err)
    }
    for _, aud := range claims.Audience {
        if u, err := url.Parse(aud); err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" {
            return strings.TrimRight(aud, "/"), nil
        }
    }
    return "", fmt.Errorf("%s must be a login or sa-session JWT whose aud is the home-region URL; found no URL-shaped audience claim", EnvTokenVar)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

package auth

import (
    "encoding/base64"
    "encoding/json"
    "testing"

"github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestCoreURLFromEnvToken(t *testing.T) {
    t.Parallel()

tests := []struct {
    name    string
    aud     any // nil omits the aud claim entirely
    want    string
    wantErr bool
}{
    {
        name: "string aud",
        aud:  "https://core.us.entire.io",
        want: "https://core.us.entire.io",
    },
    {
        name: "string aud trailing slash trimmed",
        aud:  "https://core.us.entire.io/",
        want: "https://core.us.entire.io",
    },
    {
        name: "array aud picks first URL-shaped",
        aud:  []string{"entire-cli", "https://core.eu.entire.io"},
        want: "https://core.eu.entire.io",
    },
    {
        name:    "opaque string aud rejected",
        aud:     "some-opaque-audience",
        wantErr: true,
    },
    {
        name:    "array of opaque audiences rejected",
        aud:     []string{"aud-a", "aud-b"},
        wantErr: true,
    },
    {
        name:    "missing aud rejected",
        aud:     nil,
        wantErr: true,
    },
}

for _, tc := range tests {
    t.Run(tc.name, func(t *testing.T) {
        t.Parallel()
        payload := map[string]any{"sub": "ci-runner"}
        if tc.aud != nil {
            payload["aud"] = tc.aud
        }
        raw, err := json.Marshal(payload)
        require.NoError(t, err)
        token := makeJWT(t, string(raw))

got, err := CoreURLFromEnvToken(token)
        if tc.wantErr {
            require.Error(t, err)
            assert.Contains(t, err.Error(), EnvTokenVar)
            return
        }
        require.NoError(t, err)
        assert.Equal(t, tc.want, got)
    })
}
}

func TestCoreURLFromEnvToken_MalformedToken(t *testing.T) {
    t.Parallel()
    _, err := CoreURLFromEnvToken("not-a-jwt")
    require.Error(t, err)
    assert.Contains(t, err.Error(), EnvTokenVar)
}

func TestCoreURLFromEnvToken_RejectsAlgNone(t *testing.T) {
    t.Parallel()
    // alg:none with a URL-shaped aud must still be rejected at the parse layer.
    enc := base64.RawURLEncoding
    token := enc.EncodeToString([]byte(`{"alg":"none"}`)) + "." +
        enc.EncodeToString([]byte(`{"aud":"https://core.us.entire.io"}`)) + "."
    _, err := CoreURLFromEnvToken(token)
    require.Error(t, err)
    assert.Contains(t, err.Error(), EnvTokenVar)
}

resolveCreds Functionality

// resolveCreds builds the repo-scoped token cache, choosing the auth source:
//
//   - ENTIRE_TOKEN set: use the env JWT verbatim as the login token, deriving
//     the core URL from its aud claim. Skips contexts.json and the keyring
//     entirely — the CI / workload-identity path. A non-URL aud is a hard
//     error, never a silent fallback to context resolution.
//   - otherwise: resolve the login context for this cluster from contexts.json
//     (migrating any pre-contexts.json login first) and exchange its stored
//     login JWT.
func resolveCreds(ctx context.Context, parsedURL *url.URL, clusterBaseURL string, httpClient *http.Client) (*repocreds.Cache, error) {
    if envToken := os.Getenv(auth.EnvTokenVar); envToken != "" {
        coreURL, err := auth.CoreURLFromEnvToken(envToken)
        if err != nil {
            return nil, err //nolint:wrapcheck // CoreURLFromEnvToken already returns a user-facing, ENTIRE_TOKEN-prefixed error
        }
        debuglog.Printf("authenticating via %s; core=%s", auth.EnvTokenVar, coreURL)
        return repocreds.New(coreURL, clusterBaseURL, func(context.Context) (string, error) {
            return envToken, nil
        }, httpClient), nil
    }

// Bridge any pre-contexts.json login so the resolver can find it.
    if _, err := auth.MigrateLegacyLoginContext(); err != nil {
        debuglog.Printf("legacy login migration: %v", err)
    }

// Resolve which login context authenticates this cluster: the cluster's
    // cores are taken from the cluster_cores.json cache (or a live
    // /.well-known fetch on miss/expiry), then the account is selected from
    // local contexts — active context if eligible, else the sole eligible
    // one, else an explicit-choice error.
    cfgDir := contexts.DefaultConfigDir()
    clusterCtx, err := clusterdiscovery.ResolveContextForCluster(ctx, cfgDir, discovery.DefaultCacheDir(), parsedURL.Host, httpClient, debuglog.Printf)
    if err != nil {
        return nil, err //nolint:wrapcheck // ResolveContextForCluster already returns a user-facing error; preserved verbatim for the "fatal: <msg>" surface
    }

// Mint repo-scoped tokens by exchanging the context's login JWT at its
    // core's /oauth/token, cached per (repo, action) for this invocation.
    return repocreds.New(clusterCtx.CoreURL, clusterBaseURL, func(context.Context) (string, error) {
        return auth.LoginTokenForContext(clusterCtx)
    }, httpClient), nil
}

// gitActionFromRequest classifies a smart-HTTP request as "pull" or "push" // so the right repo-scoped token can be minted. Returns "" when the // endpoint isn't a recognised git smart-HTTP route.