commands/token/rotate/rotate.go (209 lines of code) (raw):
package rotate
import (
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"gitlab.com/gitlab-org/cli/commands/token/expirationdate"
"gitlab.com/gitlab-org/cli/commands/token/filter"
"gitlab.com/gitlab-org/cli/commands/flag"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"github.com/MakeNowJust/heredoc/v2"
"github.com/spf13/cobra"
gitlab "gitlab.com/gitlab-org/api/client-go"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/internal/glrepo"
)
type RotateOptions struct {
HTTPClient func() (*gitlab.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (glrepo.Interface, error)
User string
Group string
Name any
Duration time.Duration
ExpireAt expirationdate.ExpirationDate
OutputFormat string
}
func NewCmdRotate(f *cmdutils.Factory, runE func(opts *RotateOptions) error) *cobra.Command {
opts := &RotateOptions{
IO: f.IO,
}
cmd := &cobra.Command{
Use: "rotate <token-name|token-id>",
Short: "Rotate user, group, or project access tokens",
Aliases: []string{"rotate", "rot"},
Args: cobra.RangeArgs(1, 1),
Long: heredoc.Doc(`
Rotate user, group, or project access token, then print the new token on stdout. If multiple tokens with
the same name exist, you can specify the ID of the token.
The expiration date of the token will be calculated by adding the duration (default 30 days) to the
current date. Alternatively you can specify a different duration or an explicit end date.
The output format can be either "JSON" or "text". The JSON output will show the meta information of the
rotated token.
Administrators can rotate personal access tokens belonging to other users.
`),
Example: heredoc.Doc(`
Rotate project access token of current project
- glab token rotate my-project-token
Rotate project access token of another project, set to expiration date
- glab token rotate --repo user/repo my-project-token --expires-at 2024-08-08
Rotate group access token
- glab token rotate --group group/sub-group my-group-token
Rotate personal access token and extend duration to 7 days
- glab token rotate --user @me --duration $((7 * 24))h my-personal-token
Rotate a personal access token of another user (administrator only)
- glab token rotate --user johndoe johns-personal-token
`),
RunE: func(cmd *cobra.Command, args []string) (err error) {
// Supports repo override
opts.HTTPClient = f.HttpClient
opts.BaseRepo = f.BaseRepo
if opts.Name, err = strconv.Atoi(args[0]); err != nil {
opts.Name = args[0]
}
if opts.Group, err = flag.GroupOverride(cmd); err != nil {
return
}
if opts.Group != "" && opts.User != "" {
return cmdutils.FlagError{Err: errors.New("'--group' and '--user' are mutually exclusive.")}
}
if opts.Duration.Truncate(24*time.Hour) != opts.Duration {
return cmdutils.FlagError{Err: errors.New("duration must be in days.")}
}
if opts.Duration < 24*time.Hour || opts.Duration > 365*24*time.Hour {
return cmdutils.FlagError{Err: errors.New("duration in days must be between 1 and 365.")}
}
if cmd.Flags().Changed("expires-at") && cmd.Flags().Changed("duration") {
return cmdutils.FlagError{Err: errors.New("'--expires-at' and '--duration' are mutually exclusive.")}
}
if time.Time(opts.ExpireAt).IsZero() {
opts.ExpireAt = expirationdate.ExpirationDate(time.Now().Add(opts.Duration).Truncate(time.Hour * 24))
}
if runE != nil {
return runE(opts)
}
return rotateTokenRun(opts)
},
}
cmdutils.EnableRepoOverride(cmd, f)
cmd.Flags().StringVarP(&opts.Group, "group", "g", "", "Rotate group access token. Ignored if a user or repository argument is set.")
cmd.Flags().StringVarP(&opts.User, "user", "U", "", "Rotate personal access token. Use @me for the current user.")
cmd.Flags().DurationVarP(&opts.Duration, "duration", "D", time.Duration(30*24*time.Hour), "Sets the token duration, in hours. Maximum of 8760. Examples: 24h, 168h, 504h.")
cmd.Flags().VarP(&opts.ExpireAt, "expires-at", "E", "Sets the token's expiration date and time, in YYYY-MM-DD format. If not specified, --duration is used.")
cmd.Flags().StringVarP(&opts.OutputFormat, "output", "F", "text", "Format output as: text, json. 'text' provides the new token value; 'json' outputs the token with metadata.")
return cmd
}
func rotateTokenRun(opts *RotateOptions) error {
httpClient, err := opts.HTTPClient()
if err != nil {
return err
}
expirationDate := gitlab.ISOTime(opts.ExpireAt)
var outputToken any
var outputTokenValue string
if opts.User != "" {
user, err := api.UserByName(httpClient, opts.User)
if err != nil {
return cmdutils.FlagError{Err: err}
}
options := &gitlab.ListPersonalAccessTokensOptions{
ListOptions: gitlab.ListOptions{PerPage: 100},
UserID: &user.ID,
}
tokens, err := api.ListPersonalAccessTokens(httpClient, options)
if err != nil {
return err
}
var token *gitlab.PersonalAccessToken
tokens = filter.Filter(tokens, func(t *gitlab.PersonalAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v'. Use the ID instead.", opts.Name)}
}
rotateOptions := &gitlab.RotatePersonalAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotatePersonalAccessToken(httpClient, token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
} else {
if opts.Group != "" {
options := &gitlab.ListGroupAccessTokensOptions{PerPage: 100}
tokens, err := api.ListGroupAccessTokens(httpClient, opts.Group, options)
if err != nil {
return err
}
var token *gitlab.GroupAccessToken
tokens = filter.Filter(tokens, func(t *gitlab.GroupAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v', use the ID instead", opts.Name)}
}
rotateOptions := &gitlab.RotateGroupAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotateGroupAccessToken(httpClient, opts.Group, token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
} else {
repo, err := opts.BaseRepo()
if err != nil {
return err
}
options := &gitlab.ListProjectAccessTokensOptions{ListOptions: gitlab.ListOptions{PerPage: 100}}
tokens, err := api.ListProjectAccessTokens(httpClient, repo.FullName(), options)
if err != nil {
return err
}
tokens = filter.Filter(tokens, func(t *gitlab.ProjectAccessToken) bool {
return t.Active && (t.Name == opts.Name || t.ID == opts.Name)
})
var token *gitlab.ProjectAccessToken
switch len(tokens) {
case 1:
token = tokens[0]
case 0:
return cmdutils.FlagError{Err: fmt.Errorf("no token found with the name '%v'", opts.Name)}
default:
return cmdutils.FlagError{Err: fmt.Errorf("multiple tokens found with the name '%v', use the ID instead", opts.Name)}
}
rotateOptions := &gitlab.RotateProjectAccessTokenOptions{
ExpiresAt: &expirationDate,
}
if token, err = api.RotateProjectAccessToken(httpClient, repo.FullName(), token.ID, rotateOptions); err != nil {
return err
}
outputToken = token
outputTokenValue = token.Token
}
}
if opts.OutputFormat == "json" {
encoder := json.NewEncoder(opts.IO.StdOut)
if err := encoder.Encode(outputToken); err != nil {
return err
}
} else {
if _, err := fmt.Fprintf(opts.IO.StdOut, "%s\n", outputTokenValue); err != nil {
return err
}
}
return nil
}