Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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