Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ their canonical paths are still runnable.
- `configure`: bare prints help and a hint pointing at `entire agent`; flags
manage non-agent settings (telemetry, git-hook installation mode, strategy
options, summary provider). Agent CRUD lives under `entire agent`.
- `auth`: `login`, `logout`, `status`, `contexts`, `use`. `logout` takes
`--everywhere` (revoke every session on the active core, not just the
- `auth`: `login`, `logout`, `status`, `contexts`, `use`, plus the hidden
`token` (prints the active control-plane bearer to stdout for scripting/curl;
honors `ENTIRE_TOKEN`, else the refreshed active-context login JWT). `logout`
takes `--everywhere` (revoke every session on the active core, not just the
current one) and `--all-contexts` (log out of every saved login)
- `doctor`: bare runs the scan-and-fix flow, plus `trace`, `logs`, `bundle`

Expand Down
56 changes: 56 additions & 0 deletions cmd/entire/cli/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,63 @@ func newAuthCmd() *cobra.Command {
cmd.AddCommand(newLoginCmd())
cmd.AddCommand(newLogoutCmd())
cmd.AddCommand(newAuthStatusCmd())
cmd.AddCommand(newAuthTokenCmd())
cmd.AddCommand(newAuthContextsCmd())
cmd.AddCommand(newAuthUseCmd())
return cmd
}

// --- token ------------------------------------------------------------------

// newAuthTokenCmd prints the active control-plane bearer to stdout so scripts
// (and ad-hoc curl) can authenticate against the core API without re-deriving
// the keychain slot — e.g.
//
// curl -H "Authorization: Bearer $(entire auth token)" "$CORE/api/v1/clusters"
//
// Hidden: it emits a live credential, so it's a deliberate scripting escape
// hatch, not part of the everyday surface. It resolves the same bearer the API
// client would — ENTIRE_TOKEN verbatim when set, otherwise the active context's
// login JWT, refreshed if it's near expiry — and prints nothing but the token
// (errors and the not-logged-in hint go to stderr) so command substitution
// stays clean.
func newAuthTokenCmd() *cobra.Command {
var insecureHTTPAuth bool
cmd := &cobra.Command{
Use: "token",
Short: "Print the active control-plane bearer token (for scripting)",
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
// Refresh may exchange/refresh over the network; honor the
// plain-HTTP opt-in before resolving so local dev cores work.
insecure := applyInsecureHTTPAuth(insecureHTTPAuth)
target, err := resolveAuthStatusTarget(cmd.Context(), auth.Contexts, auth.RefreshedLoginToken)
if err != nil {
return err
}
// Don't mint/print a bearer for an insecure core unless explicitly
// opted in — the token would otherwise be usable over plain HTTP.
// Mirrors `auth status`.
if !insecure && target.coreURL != "" {
if err := api.RequireSecureURL(target.coreURL); err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("login server URL check: %w", err)
}
}
if target.token == "" {
cmd.SilenceUsage = true
fmt.Fprintln(cmd.ErrOrStderr(), "Not logged in. Run 'entire login' to authenticate.")
return NewSilentError(errors.New("not logged in"))
}
fmt.Fprintln(cmd.OutOrStdout(), target.token)
return nil
},
}
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
return cmd
}

// --- status -----------------------------------------------------------------

func newAuthStatusCmd() *cobra.Command {
Expand Down Expand Up @@ -156,6 +208,9 @@ type authProfile struct {
Email string
Provider string
ProviderUserID string
// Jurisdiction is the caller's home jurisdiction slug (e.g. "eu"), used to
// pick the default mirror cluster for that jurisdiction. May be empty.
Jurisdiction string
}

// profileFetcher fetches a user's profile via GET /me on coreURL, authenticated
Expand Down Expand Up @@ -276,6 +331,7 @@ func defaultFetchProfile(ctx context.Context, coreURL, token string) (*authProfi
ProviderUserID: me.Auth.ProviderUserId,
}
p.Handle, _ = me.Global.Handle.Get()
p.Jurisdiction, _ = me.Jurisdiction.Get()
if reg, ok := me.Regional.Get(); ok {
p.DisplayName, _ = reg.DisplayName.Get()
p.Email, _ = reg.Email.Get()
Expand Down
63 changes: 63 additions & 0 deletions cmd/entire/cli/auth_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cli

import (
"bytes"
"encoding/base64"
"os"
"testing"

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

// makeTestJWT builds an unsigned JWT with the given payload JSON. ParseClaims
// (used by ENTIRE_TOKEN resolution) reads the payload without verifying the
// signature, so an unsigned token is enough to exercise the resolution path.
func makeTestJWT(t *testing.T, payloadJSON string) string {
t.Helper()
enc := base64.RawURLEncoding
header := enc.EncodeToString([]byte(`{"alg":"HS256","typ":"JWT"}`))
payload := enc.EncodeToString([]byte(payloadJSON))
return header + "." + payload + "." + enc.EncodeToString([]byte("sig"))
}

// TestAuthTokenCmd covers the hidden `entire auth token` scripting helper.
//
// Not parallel: it manipulates ENTIRE_TOKEN / ENTIRE_CONFIG_DIR.
func TestAuthTokenCmd(t *testing.T) {
// Guard against a real ENTIRE_TOKEN in the dev's environment leaking into
// the not-logged-in case; restore it afterward.
if v, ok := os.LookupEnv("ENTIRE_TOKEN"); ok {
os.Unsetenv("ENTIRE_TOKEN")
// t.Setenv can't unset, and there's no t.Unsetenv, so restore manually.
t.Cleanup(func() { os.Setenv("ENTIRE_TOKEN", v) }) //nolint:usetesting // restoring a captured value; no t.Unsetenv equivalent
}

t.Run("prints the env token verbatim", func(t *testing.T) {
token := makeTestJWT(t, `{"sub":"ci","aud":"https://core.us.entire.io"}`)
t.Setenv("ENTIRE_TOKEN", token)

cmd := newAuthTokenCmd()
var out, errOut bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&errOut)
require.NoError(t, cmd.ExecuteContext(t.Context()))
require.Equal(t, token+"\n", out.String())
require.Empty(t, errOut.String())
})

t.Run("not logged in errors silently with a hint", func(t *testing.T) {
// Isolated empty config so there's no active context to resolve.
t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())

cmd := newAuthTokenCmd()
var out, errOut bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&errOut)
err := cmd.ExecuteContext(t.Context())

var silent *SilentError
require.ErrorAs(t, err, &silent)
require.Empty(t, out.String(), "stdout must stay clean for command substitution")
require.Contains(t, errOut.String(), "Not logged in")
})
}
Loading
Loading