auth: remove `auth revoke`; redefine `logout --all` to revoke sessions · Entire
auth: remove auth revoke; redefine logout --all to revoke sessions
7c8e04a·
toothbrush·1mo ago·5 files·+126 added/-282 removed
Delete the entire auth revoke command. Session management collapses to two verbs: auth status shows active sessions, logout ends them.
Redefine the logout --all flag — it no longer removes all local contexts. Instead:
logoutrevokes the active session server-side (DELETE .../tokens/current) and removes the active context locally. (Unchanged default behaviour.)logout --alladditionally asks the server to revoke every session on the active core (list families -> delete each by id). The local side is identical to the default.
After a logout clears the active context, the next saved context is promoted to active, so running entire logout repeatedly drains every saved login in turn.
Cross-core revoke is out of scope: these endpoints target AuthBaseURL's core only, pending the COR-389 control-plane retargeting.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
9dc3b1c4312cView transcript
Changes
5
cmd/entire/cli
Mauth.go+2/-97
Mauth_context_test.go+26/-9
Mauth_test.go+1/-141
Mlogout.go+59/-28
Mlogout_test.go+38/-7
26 unmodified lines
// wrong-audience keyring token.
type authTokenLister func(ctx context.Context) ([]api.Token, error)
// authTokenRevoker revokes a single API token by id. Same bearer-
// resolution contract as authTokenLister.
type authTokenRevoker func(ctx context.Context, id string) error
// User-visible placeholder strings. Promoted to constants so tests and
// production share a single source of truth.
const (
func newAuthCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "auth",
Short: "Manage authentication and API tokens",
Long: "Authentication subcommands. Includes login, logout, status, listing tokens, and revoking tokens.",
Short: "Manage authentication",
Long: "Authentication subcommands. Includes login, logout, status, and login-context management (contexts, use).",
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
cmd.AddCommand(newLoginCmd())
cmd.AddCommand(newLogoutCmd())
cmd.AddCommand(newAuthStatusCmd())
cmd.AddCommand(newAuthRevokeCmd())
cmd.AddCommand(newAuthContextsCmd())
cmd.AddCommand(newAuthUseCmd())
return cmd
}
// --- revoke -----------------------------------------------------------------
func newAuthRevokeCmd() *cobra.Command {
var revokeCurrent bool
var insecureHTTPAuth bool
cmd := &cobra.Command{
Use: "revoke [id]",
Short: "Revoke an API token by id",
Long: "Revoke a specific API token. Use --current to revoke the token used by this CLI (equivalent to 'entire logout').",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id := ""
if len(args) == 1 {
id = args[0]
}
if id == "" && !revokeCurrent {
return cmd.Help()
}
if id != "" && revokeCurrent {
return errors.New("cannot use both <id> and --current")
}
if err := requireSecureBaseURL(insecureHTTPAuth); err != nil {
return err
}
return runAuthRevoke(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(),
auth.NewContextStore(), defaultListTokens, defaultRevokeTokenByID, defaultRevokeCurrentToken,
auth.RemoveCurrentContext, api.AuthBaseURL(), id, revokeCurrent)
},
}
cmd.Flags().BoolVar(&revokeCurrent, "current", false, "Revoke the token used by this CLI and remove the local copy")
addInsecureHTTPAuthFlag(cmd, &insecureHTTPAuth)
return cmd
}
func defaultRevokeTokenByID(ctx context.Context, id string) error {
token, err := resolveDataAPIToken(ctx)
if err != nil {
return err
}
return newAPITokensClient(token).RevokeToken(ctx, id) //nolint:wrapcheck // RevokeToken already wraps with action context
}
func runAuthRevoke(
ctx context.Context,
outW, errW io.Writer,
store tokenStore,
list authTokenLister,
revokeByID authTokenRevoker,
revokeCurrent revokeCurrentFunc,
clearContext clearContextFunc,
baseURL, id string,
current bool,
) error {
token, err := store.GetToken(baseURL)
if err != nil {
return fmt.Errorf("read keychain: %w", err)
}
if token == "" {
return fmt.Errorf("not logged in to %s; run 'entire login' first", baseURL)
}
if current {
// Revoking our own token is just logout — reuse that path so behavior
// stays identical (best-effort revoke + local delete + context clear).
return runLogout(ctx, outW, errW, store, revokeCurrent, clearContext, baseURL)
}
if err := revokeByID(ctx, id); err != nil {
return err
}
// The list endpoint requires bearer auth, so a 401 here means the id we
// just revoked was the same one this CLI is using — the local copy is now
// stale and would otherwise produce confusing 401s on every command, so
// remove both the legacy keyring entry and the active context.
if _, listErr := list(ctx); listErr != nil && api.IsHTTPErrorStatus(listErr, http.StatusUnauthorized) {
if delErr := store.DeleteToken(baseURL); delErr != nil {
return fmt.Errorf("revoked token %s but failed to remove local copy: %w", id, delErr)
}
if ctxErr := clearContext(); ctxErr != nil {
fmt.Fprintf(errW, "Warning: revoked token %s but failed to clear current context: %v\n", id, ctxErr)
}
fmt.Fprintf(outW, "Revoked token %s (this was your local token; removed from keychain).\n", id)
return nil
}
fmt.Fprintf(outW, "Revoked token %s.\n", id)
return nil
}
Test Cases
func TestRunAuthRevoke_ByIDCallsRevoker(t *testing.T) {
t.Parallel()
store := newMockTokenStore()
store.tokens[testBaseURL] = testAuthTok
var gotID string
revokeByID := func(_ context.Context, id string) error {
gotID = id
return nil
}
revokeCurrentCalled := false
revokeCurrent := func(context.Context) error {
revokeCurrentCalled = true
return nil
}
// list returns 200 → token id was someone else's, no local cleanup expected.
list := func(context.Context) ([]api.Token, error) {
return []api.Token{{ID: "other"}}, nil
}
var out, errOut bytes.Buffer
err := runAuthRevoke(context.Background(), &out, &errOut, store,
list, revokeByID, revokeCurrent, func() error { return nil }, testBaseURL, testTokenID, false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if revokeCurrentCalled {
t.Fatal("revokeCurrent should not be called when revoking by id")
}
if gotID != testTokenID {
t.Errorf("revokeByID called with id=%q, want %q", gotID, testTokenID)
}
if store.deleted[testBaseURL] {
t.Fatal("local token should NOT be deleted when revoking another token")
}
if !strings.Contains(out.String(), "Revoked token "+testTokenID) {
t.Fatalf("output = %q, want confirmation", out.String())
}
}