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 }