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

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

8d53bc4→main·

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

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)
}
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)
}