236 lines
7.9 KiB
Go
236 lines
7.9 KiB
Go
package refresh
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/git"
|
|
"github.com/cli/cli/v2/internal/authflow"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/pkg/cmd/auth/shared"
|
|
"github.com/cli/cli/v2/pkg/cmd/auth/shared/gitcredentials"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/set"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type token string
|
|
type username string
|
|
|
|
type RefreshOptions struct {
|
|
IO *iostreams.IOStreams
|
|
Config func() (gh.Config, error)
|
|
PlainHttpClient func() (*http.Client, error)
|
|
GitClient *git.Client
|
|
Prompter shared.Prompt
|
|
|
|
MainExecutable string
|
|
|
|
Hostname string
|
|
Scopes []string
|
|
RemoveScopes []string
|
|
ResetScopes bool
|
|
AuthFlow func(*http.Client, *iostreams.IOStreams, string, []string, bool, bool) (token, username, error)
|
|
|
|
Interactive bool
|
|
InsecureStorage bool
|
|
Clipboard bool
|
|
}
|
|
|
|
func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command {
|
|
opts := &RefreshOptions{
|
|
IO: f.IOStreams,
|
|
Config: f.Config,
|
|
AuthFlow: func(httpClient *http.Client, io *iostreams.IOStreams, hostname string, scopes []string, interactive bool, clipboard bool) (token, username, error) {
|
|
t, u, err := authflow.AuthFlow(httpClient, hostname, io, "", scopes, interactive, f.Browser, clipboard)
|
|
return token(t), username(u), err
|
|
},
|
|
PlainHttpClient: f.PlainHttpClient,
|
|
GitClient: f.GitClient,
|
|
Prompter: f.Prompter,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "refresh",
|
|
Args: cobra.ExactArgs(0),
|
|
Short: "Refresh stored authentication credentials",
|
|
Long: heredoc.Docf(`
|
|
Expand or fix the permission scopes for stored credentials for active account.
|
|
|
|
The %[1]s--scopes%[1]s flag accepts a comma separated list of scopes you want
|
|
your gh credentials to have. If no scopes are provided, the command
|
|
maintains previously added scopes.
|
|
|
|
The %[1]s--remove-scopes%[1]s flag accepts a comma separated list of scopes you
|
|
want to remove from your gh credentials. Scope removal is idempotent.
|
|
The minimum set of scopes (%[1]srepo%[1]s, %[1]sread:org%[1]s, and %[1]sgist%[1]s) cannot be removed.
|
|
|
|
The %[1]s--reset-scopes%[1]s flag resets the scopes for your gh credentials to
|
|
the default set of scopes for your auth flow.
|
|
|
|
If you have multiple accounts in %[1]sgh auth status%[1]s and want to refresh the credentials for an
|
|
inactive account, you will have to use %[1]sgh auth switch%[1]s to that account first before using
|
|
this command, and then switch back when you are done.
|
|
|
|
For more information on OAuth scopes, see
|
|
<https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps/>.
|
|
`, "`"),
|
|
Example: heredoc.Doc(`
|
|
# Open a browser to add write:org and read:public_key scopes
|
|
$ gh auth refresh --scopes write:org,read:public_key
|
|
|
|
# Open a browser to ensure your authentication credentials have the correct minimum scopes
|
|
$ gh auth refresh
|
|
|
|
# Open a browser to idempotently remove the delete_repo scope
|
|
$ gh auth refresh --remove-scopes delete_repo
|
|
|
|
# Open a browser to re-authenticate with the default minimum scopes
|
|
$ gh auth refresh --reset-scopes
|
|
|
|
# Open a browser to re-authenticate and copy one-time OAuth code to clipboard
|
|
$ gh auth refresh --clipboard
|
|
`),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
opts.Interactive = opts.IO.CanPrompt()
|
|
|
|
if !opts.Interactive && opts.Hostname == "" {
|
|
return cmdutil.FlagErrorf("--hostname required when not running interactively")
|
|
}
|
|
|
|
opts.MainExecutable = f.ExecutablePath
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return refreshRun(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The GitHub host to use for authentication")
|
|
cmd.Flags().StringSliceVarP(&opts.Scopes, "scopes", "s", nil, "Additional authentication scopes for gh to have")
|
|
cmd.Flags().StringSliceVarP(&opts.RemoveScopes, "remove-scopes", "r", nil, "Authentication scopes to remove from gh")
|
|
cmd.Flags().BoolVar(&opts.ResetScopes, "reset-scopes", false, "Reset authentication scopes to the default minimum set of scopes")
|
|
cmd.Flags().BoolVarP(&opts.Clipboard, "clipboard", "c", false, "Copy one-time OAuth device code to clipboard")
|
|
// secure storage became the default on 2023/4/04; this flag is left as a no-op for backwards compatibility
|
|
var secureStorage bool
|
|
cmd.Flags().BoolVar(&secureStorage, "secure-storage", false, "Save authentication credentials in secure credential store")
|
|
_ = cmd.Flags().MarkHidden("secure-storage")
|
|
|
|
cmd.Flags().BoolVarP(&opts.InsecureStorage, "insecure-storage", "", false, "Save authentication credentials in plain text instead of credential store")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func refreshRun(opts *RefreshOptions) error {
|
|
plainHTTPClient, err := opts.PlainHttpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cfg, err := opts.Config()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authCfg := cfg.Authentication()
|
|
|
|
candidates := authCfg.Hosts()
|
|
if len(candidates) == 0 {
|
|
return fmt.Errorf("not logged in to any hosts. Use 'gh auth login' to authenticate with a host")
|
|
}
|
|
|
|
hostname := opts.Hostname
|
|
if hostname == "" {
|
|
if len(candidates) == 1 {
|
|
hostname = candidates[0]
|
|
} else {
|
|
selected, err := opts.Prompter.Select("What account do you want to refresh auth for?", "", candidates)
|
|
if err != nil {
|
|
return fmt.Errorf("could not prompt: %w", err)
|
|
}
|
|
hostname = candidates[selected]
|
|
}
|
|
} else {
|
|
var found bool
|
|
for _, c := range candidates {
|
|
if c == hostname {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("not logged in to %s. use 'gh auth login' to authenticate with this host", hostname)
|
|
}
|
|
}
|
|
|
|
if src, writeable := shared.AuthTokenWriteable(authCfg, hostname); !writeable {
|
|
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", src)
|
|
fmt.Fprint(opts.IO.ErrOut, "To refresh credentials stored in GitHub CLI, first clear the value from the environment.\n")
|
|
return cmdutil.SilentError
|
|
}
|
|
|
|
additionalScopes := set.NewStringSet()
|
|
|
|
if !opts.ResetScopes {
|
|
if oldToken, _ := authCfg.ActiveToken(hostname); oldToken != "" {
|
|
if oldScopes, err := shared.GetScopes(plainHTTPClient, hostname, oldToken); err == nil {
|
|
for _, s := range strings.Split(oldScopes, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if s != "" {
|
|
additionalScopes.Add(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
credentialFlow := &shared.GitCredentialFlow{
|
|
Prompter: opts.Prompter,
|
|
HelperConfig: &gitcredentials.HelperConfig{
|
|
SelfExecutablePath: opts.MainExecutable,
|
|
GitClient: opts.GitClient,
|
|
},
|
|
Updater: &gitcredentials.Updater{
|
|
GitClient: opts.GitClient,
|
|
},
|
|
}
|
|
gitProtocol := cfg.GitProtocol(hostname).Value
|
|
if opts.Interactive && gitProtocol == "https" {
|
|
if err := credentialFlow.Prompt(hostname); err != nil {
|
|
return err
|
|
}
|
|
additionalScopes.AddValues(credentialFlow.Scopes())
|
|
}
|
|
|
|
additionalScopes.AddValues(opts.Scopes)
|
|
|
|
additionalScopes.RemoveValues(opts.RemoveScopes)
|
|
|
|
authedToken, authedUser, err := opts.AuthFlow(plainHTTPClient, opts.IO, hostname, additionalScopes.ToSlice(), opts.Interactive, opts.Clipboard)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
activeUser, _ := authCfg.ActiveUser(hostname)
|
|
if activeUser != "" && username(activeUser) != authedUser {
|
|
return fmt.Errorf("error refreshing credentials for %s, received credentials for %s, did you use the correct account in the browser?", activeUser, authedUser)
|
|
}
|
|
if _, err := authCfg.Login(hostname, string(authedUser), string(authedToken), "", !opts.InsecureStorage); err != nil {
|
|
return err
|
|
}
|
|
|
|
cs := opts.IO.ColorScheme()
|
|
fmt.Fprintf(opts.IO.ErrOut, "%s Authentication complete.\n", cs.SuccessIcon())
|
|
|
|
if credentialFlow.ShouldSetup() {
|
|
username, _ := authCfg.ActiveUser(hostname)
|
|
password, _ := authCfg.ActiveToken(hostname)
|
|
if err := credentialFlow.Setup(hostname, username, password); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|