Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion internal/config/from_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ const (
GITHUB_ENTERPRISE_TOKEN = "GITHUB_ENTERPRISE_TOKEN"
)

type ReadOnlyEnvError struct {
Variable string
}

func (e *ReadOnlyEnvError) Error() string {
return fmt.Sprintf("read-only value in %s", e.Variable)
}

func InheritEnv(c Config) Config {
return &envConfig{Config: c}
}
Expand Down Expand Up @@ -56,7 +64,7 @@ func (c *envConfig) GetWithSource(hostname, key string) (string, string, error)
func (c *envConfig) CheckWriteable(hostname, key string) error {
if hostname != "" && key == "oauth_token" {
if token, env := AuthTokenFromEnv(hostname); token != "" {
return fmt.Errorf("read-only token in %s cannot be modified", env)
return &ReadOnlyEnvError{Variable: env}
}
}

Expand Down
121 changes: 66 additions & 55 deletions pkg/cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,70 +120,56 @@ func loginRun(opts *LoginOptions) error {
return err
}

if opts.Token != "" {
// I chose to not error on existing host here; my thinking is that for --with-token the user
// probably doesn't care if a token is overwritten since they have a token in hand they
// explicitly want to use.
if opts.Hostname == "" {
return errors.New("empty hostname would leak oauth_token")
hostname := opts.Hostname
if hostname == "" {
if opts.Interactive {
var err error
hostname, err = promptForHostname()
if err != nil {
return err
}
} else {
return errors.New("must specify --hostname")
}
}

err := cfg.Set(opts.Hostname, "oauth_token", opts.Token)
if err != nil {
return err
if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
var roErr *config.ReadOnlyEnvError
if errors.As(err, &roErr) {
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
fmt.Fprint(opts.IO.ErrOut, "To have GitHub CLI store credentials instead, first clear the value from the environment.\n")
return cmdutil.SilentError
}
return err
}

err = shared.ValidateHostCfg(opts.Hostname, cfg)
if opts.Token != "" {
err := cfg.Set(hostname, "oauth_token", opts.Token)
if err != nil {
return err
}

return cfg.Write()
}

// TODO consider explicitly telling survey what io to use since it's implicit right now

hostname := opts.Hostname

if hostname == "" {
var hostType int
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to log into?",
Options: []string{
"GitHub.com",
"GitHub Enterprise Server",
},
}, &hostType)

apiClient, err := shared.ClientFromCfg(hostname, cfg)
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
return err
}

isEnterprise := hostType == 1

hostname = ghinstance.Default()
if isEnterprise {
err := prompt.SurveyAskOne(&survey.Input{
Message: "GHE hostname:",
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
if err != nil {
return fmt.Errorf("could not prompt: %w", err)
}
if err := apiClient.HasMinimumScopes(hostname); err != nil {
return fmt.Errorf("error validating token: %w", err)
}
}

fmt.Fprintf(opts.IO.ErrOut, "- Logging into %s\n", hostname)
return cfg.Write()
}

existingToken, _ := cfg.Get(hostname, "oauth_token")

if existingToken != "" && opts.Interactive {
err := shared.ValidateHostCfg(hostname, cfg)
if err == nil {
apiClient, err := shared.ClientFromCfg(hostname, cfg)
if err != nil {
return err
}
apiClient, err := shared.ClientFromCfg(hostname, cfg)
if err != nil {
return err
}

if err := apiClient.HasMinimumScopes(hostname); err == nil {
username, err := api.CurrentLoginName(apiClient, hostname)
if err != nil {
return fmt.Errorf("error using api: %w", err)
Expand All @@ -206,10 +192,6 @@ func loginRun(opts *LoginOptions) error {
}
}

if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
return err
}

var authMode int
if opts.Web {
authMode = 0
Expand Down Expand Up @@ -244,19 +226,19 @@ func loginRun(opts *LoginOptions) error {
return fmt.Errorf("could not prompt: %w", err)
}

if hostname == "" {
return errors.New("empty hostname would leak oauth_token")
}

err = cfg.Set(hostname, "oauth_token", token)
if err != nil {
return err
}

err = shared.ValidateHostCfg(hostname, cfg)
apiClient, err := shared.ClientFromCfg(hostname, cfg)
if err != nil {
return err
}

if err := apiClient.HasMinimumScopes(hostname); err != nil {
return fmt.Errorf("error validating token: %w", err)
}
}

cs := opts.IO.ColorScheme()
Expand Down Expand Up @@ -322,6 +304,35 @@ func loginRun(opts *LoginOptions) error {
return nil
}

func promptForHostname() (string, error) {
var hostType int
err := prompt.SurveyAskOne(&survey.Select{
Message: "What account do you want to log into?",
Options: []string{
"GitHub.com",
"GitHub Enterprise Server",
},
}, &hostType)

if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}

isEnterprise := hostType == 1

hostname := ghinstance.Default()
if isEnterprise {
err := prompt.SurveyAskOne(&survey.Input{
Message: "GHE hostname:",
}, &hostname, survey.WithValidator(ghinstance.HostnameValidator))
if err != nil {
return "", fmt.Errorf("could not prompt: %w", err)
}
}

return hostname, nil
}

func getAccessTokenTip(hostname string) string {
ghHostname := hostname
if ghHostname == "" {
Expand Down
75 changes: 63 additions & 12 deletions pkg/cmd/auth/login/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package login
import (
"bytes"
"net/http"
"os"
"regexp"
"testing"

"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/api"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/pkg/cmd/auth/shared"
Expand Down Expand Up @@ -189,18 +191,23 @@ func Test_NewCmdLogin(t *testing.T) {

func Test_loginRun_nontty(t *testing.T) {
tests := []struct {
name string
opts *LoginOptions
httpStubs func(*httpmock.Registry)
wantHosts string
wantErr string
name string
opts *LoginOptions
httpStubs func(*httpmock.Registry)
env map[string]string
wantHosts string
wantErr string
wantStderr string
}{
{
name: "with token",
opts: &LoginOptions{
Hostname: "github.com",
Token: "abc123",
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
},
wantHosts: "github.com:\n oauth_token: abc123\n",
},
{
Expand All @@ -223,7 +230,7 @@ func Test_loginRun_nontty(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org"))
},
wantErr: `could not validate token: missing required scope 'repo'`,
wantErr: `error validating token: missing required scope 'repo'`,
},
{
name: "missing read scope",
Expand All @@ -234,7 +241,7 @@ func Test_loginRun_nontty(t *testing.T) {
httpStubs: func(reg *httpmock.Registry) {
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo"))
},
wantErr: `could not validate token: missing required scope 'read:org'`,
wantErr: `error validating token: missing required scope 'read:org'`,
},
{
name: "has admin scope",
Expand All @@ -247,6 +254,36 @@ func Test_loginRun_nontty(t *testing.T) {
},
wantHosts: "github.com:\n oauth_token: abc456\n",
},
{
name: "github.com token from environment",
opts: &LoginOptions{
Hostname: "github.com",
Token: "abc456",
},
env: map[string]string{
"GH_TOKEN": "value_from_env",
},
wantErr: "SilentError",
wantStderr: heredoc.Doc(`
The value of the GH_TOKEN environment variable is being used for authentication.
To have GitHub CLI store credentials instead, first clear the value from the environment.
`),
},
{
name: "GHE token from environment",
opts: &LoginOptions{
Hostname: "ghe.io",
Token: "abc456",
},
env: map[string]string{
"GH_ENTERPRISE_TOKEN": "value_from_env",
},
wantErr: "SilentError",
wantStderr: heredoc.Doc(`
The value of the GH_ENTERPRISE_TOKEN environment variable is being used for authentication.
To have GitHub CLI store credentials instead, first clear the value from the environment.
`),
},
}

for _, tt := range tests {
Expand All @@ -256,7 +293,8 @@ func Test_loginRun_nontty(t *testing.T) {
io.SetStdoutTTY(false)

tt.opts.Config = func() (config.Config, error) {
return config.NewBlankConfig(), nil
cfg := config.NewBlankConfig()
return config.InheritEnv(cfg), nil
}

tt.opts.IO = io
Expand All @@ -271,10 +309,23 @@ func Test_loginRun_nontty(t *testing.T) {
return api.NewClientFromHTTP(httpClient), nil
}

old_GH_TOKEN := os.Getenv("GH_TOKEN")
os.Setenv("GH_TOKEN", tt.env["GH_TOKEN"])
old_GITHUB_TOKEN := os.Getenv("GITHUB_TOKEN")
os.Setenv("GITHUB_TOKEN", tt.env["GITHUB_TOKEN"])
old_GH_ENTERPRISE_TOKEN := os.Getenv("GH_ENTERPRISE_TOKEN")
os.Setenv("GH_ENTERPRISE_TOKEN", tt.env["GH_ENTERPRISE_TOKEN"])
old_GITHUB_ENTERPRISE_TOKEN := os.Getenv("GITHUB_ENTERPRISE_TOKEN")
os.Setenv("GITHUB_ENTERPRISE_TOKEN", tt.env["GITHUB_ENTERPRISE_TOKEN"])
defer func() {
os.Setenv("GH_TOKEN", old_GH_TOKEN)
os.Setenv("GITHUB_TOKEN", old_GITHUB_TOKEN)
os.Setenv("GH_ENTERPRISE_TOKEN", old_GH_ENTERPRISE_TOKEN)
os.Setenv("GITHUB_ENTERPRISE_TOKEN", old_GITHUB_ENTERPRISE_TOKEN)
}()

if tt.httpStubs != nil {
tt.httpStubs(reg)
} else {
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
}

mainBuf := bytes.Buffer{}
Expand All @@ -289,7 +340,7 @@ func Test_loginRun_nontty(t *testing.T) {
}

assert.Equal(t, "", stdout.String())
assert.Equal(t, "", stderr.String())
assert.Equal(t, tt.wantStderr, stderr.String())
assert.Equal(t, tt.wantHosts, hostsBuf.String())
reg.Verify(t)
})
Expand Down Expand Up @@ -325,7 +376,7 @@ func Test_loginRun_Survey(t *testing.T) {
as.StubOne(false) // do not continue
},
wantHosts: "", // nothing should have been written to hosts
wantErrOut: regexp.MustCompile("Logging into github.com"),
wantErrOut: nil,
},
{
name: "hostname set",
Expand Down
6 changes: 6 additions & 0 deletions pkg/cmd/auth/logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ func logoutRun(opts *LogoutOptions) error {
}

if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
var roErr *config.ReadOnlyEnvError
if errors.As(err, &roErr) {
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
fmt.Fprint(opts.IO.ErrOut, "To erase credentials stored in GitHub CLI, first clear the value from the environment.\n")
return cmdutil.SilentError
}
return err
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/cmd/auth/refresh/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ func refreshRun(opts *RefreshOptions) error {
}

if err := cfg.CheckWriteable(hostname, "oauth_token"); err != nil {
var roErr *config.ReadOnlyEnvError
if errors.As(err, &roErr) {
fmt.Fprintf(opts.IO.ErrOut, "The value of the %s environment variable is being used for authentication.\n", roErr.Variable)
fmt.Fprint(opts.IO.ErrOut, "To refresh credentials stored in GitHub CLI, first clear the value from the environment.\n")
return cmdutil.SilentError
}
return err
}

Expand Down
14 changes: 0 additions & 14 deletions pkg/cmd/auth/shared/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,6 @@ import (
"github.com/cli/cli/internal/config"
)

func ValidateHostCfg(hostname string, cfg config.Config) error {
apiClient, err := ClientFromCfg(hostname, cfg)
if err != nil {
return err
}

err = apiClient.HasMinimumScopes(hostname)
if err != nil {
return fmt.Errorf("could not validate token: %w", err)
}

return nil
}

var ClientFromCfg = func(hostname string, cfg config.Config) (*api.Client, error) {
var opts []api.ClientOption

Expand Down