Files
forge/cmd/admin_user_delete_access_token.go
zeekayandHanzo Dev d85a6e9498 List and delete a user's access tokens from the CLI
Generating a token was a command; seeing what existed and revoking one were
not, so a token could only be created and never audited or withdrawn. Both
gaps are the same shape and both are closed here: list prints id, name, scope
and dates for one named user, and delete takes exactly one of --token-name or
--id and refuses an id that belongs to somebody else.

Scope stays narrow by construction: ListAccessTokensOptions requires a UserID,
so one user's tokens are the widest set either command can reach.

generate-access-token gains the initDB guard its siblings already had. Without
it the test harness — which supplies its own database — re-enters MustInstalled
and log.Fatal kills the process, so the new tests could not run against it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 13:50:06 -07:00

90 lines
2.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"context"
"errors"
"fmt"
auth_model "github.com/hanzoai/git/models/auth"
"github.com/hanzoai/git/models/db"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/setting"
"github.com/urfave/cli/v3"
)
func newUserDeleteAccessTokenCommand() *cli.Command {
return &cli.Command{
Name: "delete-access-token",
Usage: "Delete an access token of a specific user",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Usage: "Username",
},
&cli.StringFlag{
Name: "token-name",
Aliases: []string{"t"},
Usage: "Token name, as shown by list-access-tokens",
},
&cli.Int64Flag{
Name: "id",
Usage: "Token id, as shown by list-access-tokens",
},
},
Action: runDeleteAccessToken,
}
}
func runDeleteAccessToken(ctx context.Context, c *cli.Command) error {
if !c.IsSet("username") {
return errors.New("you must provide a username to delete a token of")
}
if c.IsSet("token-name") == c.IsSet("id") {
return errors.New("you must provide exactly one of --token-name or --id")
}
// The test harness builds its own database, so initDB there re-enters
// MustInstalled and kills the process before the command runs. Every other
// admin command guards it the same way.
if !setting.IsInTesting {
if err := initDB(ctx); err != nil {
return err
}
}
user, err := user_model.GetUserByName(ctx, c.String("username"))
if err != nil {
return err
}
id := c.Int64("id")
if c.IsSet("token-name") {
// Name is unique per user, so this resolves to at most one token.
tokens, err := db.Find[auth_model.AccessToken](ctx, auth_model.ListAccessTokensOptions{
Name: c.String("token-name"),
UserID: user.ID,
})
if err != nil {
return err
}
if len(tokens) == 0 {
return fmt.Errorf("user %q has no access token named %q", user.Name, c.String("token-name"))
}
id = tokens[0].ID
}
// Scoped by user id as well as token id, so an id belonging to another
// user cannot be deleted through this command.
if err := auth_model.DeleteAccessTokenByID(ctx, id, user.ID); err != nil {
return err
}
fmt.Printf("Access token %d of user %s was deleted\n", id, user.Name)
return nil
}