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:

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

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