Add ability to store tokens in encrypted storage (#7043)
This commit is contained in:
parent
a33e12a21d
commit
df83dc2d58
13 changed files with 386 additions and 118 deletions
|
|
@ -30,11 +30,12 @@ type LoginOptions struct {
|
|||
|
||||
Interactive bool
|
||||
|
||||
Hostname string
|
||||
Scopes []string
|
||||
Token string
|
||||
Web bool
|
||||
GitProtocol string
|
||||
Hostname string
|
||||
Scopes []string
|
||||
Token string
|
||||
Web bool
|
||||
GitProtocol string
|
||||
SecureStorage bool
|
||||
}
|
||||
|
||||
func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
|
||||
|
|
@ -123,6 +124,7 @@ func NewCmdLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Comm
|
|||
cmd.Flags().BoolVar(&tokenStdin, "with-token", false, "Read token from standard input")
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open a browser to authenticate")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.GitProtocol, "git-protocol", "p", "", []string{"ssh", "https"}, "The protocol to use for git operations")
|
||||
cmd.Flags().BoolVarP(&opts.SecureStorage, "secure-storage", "", false, "Save authentication credentials in secure credential store")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -134,6 +136,11 @@ func loginRun(opts *LoginOptions) error {
|
|||
}
|
||||
authCfg := cfg.Authentication()
|
||||
|
||||
if opts.SecureStorage {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s Using secure storage could break installed extensions\n", cs.WarningIcon())
|
||||
}
|
||||
|
||||
hostname := opts.Hostname
|
||||
if opts.Interactive && hostname == "" {
|
||||
var err error
|
||||
|
|
@ -158,8 +165,8 @@ func loginRun(opts *LoginOptions) error {
|
|||
if err := shared.HasMinimumScopes(httpClient, hostname, opts.Token); err != nil {
|
||||
return fmt.Errorf("error validating token: %w", err)
|
||||
}
|
||||
|
||||
return authCfg.Login(hostname, "", opts.Token, opts.GitProtocol, false)
|
||||
// Adding a user key ensures that a nonempty host section gets written to the config file.
|
||||
return authCfg.Login(hostname, "x-access-token", opts.Token, opts.GitProtocol, opts.SecureStorage)
|
||||
}
|
||||
|
||||
existingToken, _ := authCfg.Token(hostname)
|
||||
|
|
@ -176,18 +183,19 @@ func loginRun(opts *LoginOptions) error {
|
|||
}
|
||||
|
||||
return shared.Login(&shared.LoginOptions{
|
||||
IO: opts.IO,
|
||||
Config: authCfg,
|
||||
HTTPClient: httpClient,
|
||||
Hostname: hostname,
|
||||
Interactive: opts.Interactive,
|
||||
Web: opts.Web,
|
||||
Scopes: opts.Scopes,
|
||||
Executable: opts.MainExecutable,
|
||||
GitProtocol: opts.GitProtocol,
|
||||
Prompter: opts.Prompter,
|
||||
GitClient: opts.GitClient,
|
||||
Browser: opts.Browser,
|
||||
IO: opts.IO,
|
||||
Config: authCfg,
|
||||
HTTPClient: httpClient,
|
||||
Hostname: hostname,
|
||||
Interactive: opts.Interactive,
|
||||
Web: opts.Web,
|
||||
Scopes: opts.Scopes,
|
||||
Executable: opts.MainExecutable,
|
||||
GitProtocol: opts.GitProtocol,
|
||||
Prompter: opts.Prompter,
|
||||
GitClient: opts.GitClient,
|
||||
Browser: opts.Browser,
|
||||
SecureStorage: opts.SecureStorage,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func stubHomeDir(t *testing.T, dir string) {
|
||||
|
|
@ -172,6 +173,23 @@ func Test_NewCmdLogin(t *testing.T) {
|
|||
Interactive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tty secure-storage",
|
||||
stdinTTY: true,
|
||||
cli: "--secure-storage",
|
||||
wants: LoginOptions{
|
||||
Interactive: true,
|
||||
SecureStorage: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nontty secure-storage",
|
||||
cli: "--secure-storage",
|
||||
wants: LoginOptions{
|
||||
Hostname: "github.com",
|
||||
SecureStorage: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -223,13 +241,14 @@ func Test_NewCmdLogin(t *testing.T) {
|
|||
|
||||
func Test_loginRun_nontty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
wantHosts string
|
||||
wantErr string
|
||||
wantStderr string
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
wantHosts string
|
||||
wantErr string
|
||||
wantStderr string
|
||||
wantSecureToken string
|
||||
}{
|
||||
{
|
||||
name: "with token",
|
||||
|
|
@ -240,7 +259,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
},
|
||||
wantHosts: "github.com:\n oauth_token: abc123\n",
|
||||
wantHosts: "github.com:\n oauth_token: abc123\n user: x-access-token\n",
|
||||
},
|
||||
{
|
||||
name: "with token and https git-protocol",
|
||||
|
|
@ -252,7 +271,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
},
|
||||
wantHosts: "github.com:\n oauth_token: abc123\n git_protocol: https\n",
|
||||
wantHosts: "github.com:\n oauth_token: abc123\n user: x-access-token\n git_protocol: https\n",
|
||||
},
|
||||
{
|
||||
name: "with token and non-default host",
|
||||
|
|
@ -263,7 +282,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
|
||||
},
|
||||
wantHosts: "albert.wesker:\n oauth_token: abc123\n",
|
||||
wantHosts: "albert.wesker:\n oauth_token: abc123\n user: x-access-token\n",
|
||||
},
|
||||
{
|
||||
name: "missing repo scope",
|
||||
|
|
@ -296,7 +315,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org"))
|
||||
},
|
||||
wantHosts: "github.com:\n oauth_token: abc456\n",
|
||||
wantHosts: "github.com:\n oauth_token: abc456\n user: x-access-token\n",
|
||||
},
|
||||
{
|
||||
name: "github.com token from environment",
|
||||
|
|
@ -336,15 +355,30 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
To have GitHub CLI store credentials instead, first clear the value from the environment.
|
||||
`),
|
||||
},
|
||||
{
|
||||
name: "with token and secure storage",
|
||||
opts: &LoginOptions{
|
||||
Hostname: "github.com",
|
||||
Token: "abc123",
|
||||
SecureStorage: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
|
||||
},
|
||||
wantHosts: "github.com:\n user: x-access-token\n",
|
||||
wantSecureToken: "abc123",
|
||||
wantStderr: "! Using secure storage could break installed extensions\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(false)
|
||||
ios.SetStdoutTTY(false)
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, stderr := iostreams.Test()
|
||||
ios.SetStdinTTY(false)
|
||||
ios.SetStdoutTTY(false)
|
||||
tt.opts.IO = ios
|
||||
|
||||
keyring.MockInit()
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
cfg := config.NewBlankConfig()
|
||||
if tt.cfgStubs != nil {
|
||||
|
|
@ -355,6 +389,7 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
defer reg.Verify(t)
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
}
|
||||
|
|
@ -375,11 +410,12 @@ func Test_loginRun_nontty(t *testing.T) {
|
|||
mainBuf := bytes.Buffer{}
|
||||
hostsBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, &hostsBuf)
|
||||
secureToken, _ := cfg.Authentication().TokenFromKeyring(tt.opts.Hostname)
|
||||
|
||||
assert.Equal(t, "", stdout.String())
|
||||
assert.Equal(t, tt.wantStderr, stderr.String())
|
||||
assert.Equal(t, tt.wantHosts, hostsBuf.String())
|
||||
reg.Verify(t)
|
||||
assert.Equal(t, tt.wantSecureToken, secureToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -388,14 +424,15 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
stubHomeDir(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
runStubs func(*run.CommandStubber)
|
||||
wantHosts string
|
||||
wantErrOut *regexp.Regexp
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
name string
|
||||
opts *LoginOptions
|
||||
httpStubs func(*httpmock.Registry)
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
runStubs func(*run.CommandStubber)
|
||||
cfgStubs func(*config.ConfigMock)
|
||||
wantHosts string
|
||||
wantErrOut *regexp.Regexp
|
||||
wantSecureToken string
|
||||
}{
|
||||
{
|
||||
name: "already authenticated",
|
||||
|
|
@ -553,32 +590,62 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
},
|
||||
wantErrOut: regexp.MustCompile("Tip: you can generate a Personal Access Token here https://github.com/settings/tokens"),
|
||||
},
|
||||
// TODO how to test browser auth?
|
||||
{
|
||||
name: "secure storage",
|
||||
opts: &LoginOptions{
|
||||
Hostname: "github.com",
|
||||
Interactive: true,
|
||||
SecureStorage: true,
|
||||
},
|
||||
prompterStubs: func(pm *prompter.PrompterMock) {
|
||||
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
|
||||
switch prompt {
|
||||
case "What is your preferred protocol for Git operations?":
|
||||
return prompter.IndexFor(opts, "HTTPS")
|
||||
case "How would you like to authenticate GitHub CLI?":
|
||||
return prompter.IndexFor(opts, "Paste an authentication token")
|
||||
}
|
||||
return -1, prompter.NoSuchPromptErr(prompt)
|
||||
}
|
||||
},
|
||||
runStubs: func(rs *run.CommandStubber) {
|
||||
rs.Register(`git config credential\.https:/`, 1, "")
|
||||
rs.Register(`git config credential\.helper`, 1, "")
|
||||
},
|
||||
wantHosts: heredoc.Doc(`
|
||||
github.com:
|
||||
user: jillv
|
||||
git_protocol: https
|
||||
`),
|
||||
wantErrOut: regexp.MustCompile("! Using secure storage could break installed extensions"),
|
||||
wantSecureToken: "def456",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.opts == nil {
|
||||
tt.opts = &LoginOptions{}
|
||||
}
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
tt.opts.IO = ios
|
||||
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewBlankConfig()
|
||||
if tt.cfgStubs != nil {
|
||||
tt.cfgStubs(cfg)
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.opts == nil {
|
||||
tt.opts = &LoginOptions{}
|
||||
}
|
||||
ios, _, _, stderr := iostreams.Test()
|
||||
|
||||
ios.SetStdinTTY(true)
|
||||
ios.SetStderrTTY(true)
|
||||
ios.SetStdoutTTY(true)
|
||||
|
||||
tt.opts.IO = ios
|
||||
|
||||
keyring.MockInit()
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
|
||||
cfg := config.NewBlankConfig()
|
||||
if tt.cfgStubs != nil {
|
||||
tt.cfgStubs(cfg)
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
reg := &httpmock.Registry{}
|
||||
tt.opts.HttpClient = func() (*http.Client, error) {
|
||||
return &http.Client{Transport: reg}, nil
|
||||
|
|
@ -620,8 +687,10 @@ func Test_loginRun_Survey(t *testing.T) {
|
|||
mainBuf := bytes.Buffer{}
|
||||
hostsBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, &hostsBuf)
|
||||
secureToken, _ := cfg.Authentication().TokenFromKeyring(tt.opts.Hostname)
|
||||
|
||||
assert.Equal(t, tt.wantHosts, hostsBuf.String())
|
||||
assert.Equal(t, tt.wantSecureToken, secureToken)
|
||||
if tt.wantErrOut == nil {
|
||||
assert.Equal(t, "", stderr.String())
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package logout
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func Test_NewCmdLogout(t *testing.T) {
|
||||
|
|
@ -96,6 +98,7 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
opts *LogoutOptions
|
||||
prompterStubs func(*prompter.PrompterMock)
|
||||
cfgHosts []string
|
||||
secureStorage bool
|
||||
wantHosts string
|
||||
wantErrOut *regexp.Regexp
|
||||
wantErr string
|
||||
|
|
@ -133,14 +136,31 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
wantHosts: "github.com:\n oauth_token: abc123\n",
|
||||
wantErrOut: regexp.MustCompile(`Logged out of cheryl.mason account 'cybilb'`),
|
||||
},
|
||||
{
|
||||
name: "secure storage",
|
||||
secureStorage: true,
|
||||
opts: &LogoutOptions{
|
||||
Hostname: "github.com",
|
||||
},
|
||||
cfgHosts: []string{"github.com"},
|
||||
wantHosts: "{}\n",
|
||||
wantErrOut: regexp.MustCompile(`Logged out of github.com account 'cybilb'`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
keyring.MockInit()
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
cfg := config.NewFromString("")
|
||||
for _, hostname := range tt.cfgHosts {
|
||||
cfg.Set(hostname, "oauth_token", "abc123")
|
||||
if tt.secureStorage {
|
||||
cfg.Set(hostname, "user", "monalisa")
|
||||
_ = keyring.Set(fmt.Sprintf("gh:%s", hostname), "", "abc123")
|
||||
cfg.Authentication().SetToken("abc123", "keyring")
|
||||
} else {
|
||||
cfg.Set(hostname, "oauth_token", "abc123")
|
||||
}
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
|
|
@ -183,8 +203,10 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
mainBuf := bytes.Buffer{}
|
||||
hostsBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, &hostsBuf)
|
||||
secureToken, _ := cfg.Authentication().TokenFromKeyring(tt.opts.Hostname)
|
||||
|
||||
assert.Equal(t, tt.wantHosts, hostsBuf.String())
|
||||
assert.Equal(t, "", secureToken)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
@ -192,12 +214,13 @@ func Test_logoutRun_tty(t *testing.T) {
|
|||
|
||||
func Test_logoutRun_nontty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts *LogoutOptions
|
||||
cfgHosts []string
|
||||
wantHosts string
|
||||
wantErr string
|
||||
ghtoken string
|
||||
name string
|
||||
opts *LogoutOptions
|
||||
cfgHosts []string
|
||||
secureStorage bool
|
||||
ghtoken string
|
||||
wantHosts string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "hostname, one host",
|
||||
|
|
@ -222,14 +245,30 @@ func Test_logoutRun_nontty(t *testing.T) {
|
|||
},
|
||||
wantErr: `not logged in to any hosts`,
|
||||
},
|
||||
{
|
||||
name: "secure storage",
|
||||
secureStorage: true,
|
||||
opts: &LogoutOptions{
|
||||
Hostname: "harry.mason",
|
||||
},
|
||||
cfgHosts: []string{"harry.mason"},
|
||||
wantHosts: "{}\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
keyring.MockInit()
|
||||
readConfigs := config.StubWriteConfig(t)
|
||||
cfg := config.NewFromString("")
|
||||
for _, hostname := range tt.cfgHosts {
|
||||
cfg.Set(hostname, "oauth_token", "abc123")
|
||||
if tt.secureStorage {
|
||||
cfg.Set(hostname, "user", "monalisa")
|
||||
_ = keyring.Set(fmt.Sprintf("gh:%s", hostname), "", "abc123")
|
||||
cfg.Authentication().SetToken("abc123", "keyring")
|
||||
} else {
|
||||
cfg.Set(hostname, "oauth_token", "abc123")
|
||||
}
|
||||
}
|
||||
tt.opts.Config = func() (config.Config, error) {
|
||||
return cfg, nil
|
||||
|
|
@ -257,8 +296,10 @@ func Test_logoutRun_nontty(t *testing.T) {
|
|||
mainBuf := bytes.Buffer{}
|
||||
hostsBuf := bytes.Buffer{}
|
||||
readConfigs(&mainBuf, &hostsBuf)
|
||||
secureToken, _ := cfg.Authentication().TokenFromKeyring(tt.opts.Hostname)
|
||||
|
||||
assert.Equal(t, tt.wantHosts, hostsBuf.String())
|
||||
assert.Equal(t, "", secureToken)
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,21 +26,26 @@ type RefreshOptions struct {
|
|||
|
||||
Hostname string
|
||||
Scopes []string
|
||||
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool) error
|
||||
AuthFlow func(*config.AuthConfig, *iostreams.IOStreams, string, []string, bool, bool) error
|
||||
|
||||
Interactive bool
|
||||
Interactive bool
|
||||
SecureStorage bool
|
||||
}
|
||||
|
||||
func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.Command {
|
||||
opts := &RefreshOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
AuthFlow: func(authCfg *config.AuthConfig, io *iostreams.IOStreams, hostname string, scopes []string, interactive bool) error {
|
||||
AuthFlow: func(authCfg *config.AuthConfig, io *iostreams.IOStreams, hostname string, scopes []string, interactive, secureStorage bool) error {
|
||||
if secureStorage {
|
||||
cs := io.ColorScheme()
|
||||
fmt.Fprintf(io.ErrOut, "%s Using secure storage could break installed extensions", cs.WarningIcon())
|
||||
}
|
||||
token, username, err := authflow.AuthFlow(hostname, io, "", scopes, interactive, f.Browser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return authCfg.Login(hostname, username, token, "", false)
|
||||
return authCfg.Login(hostname, username, token, "", secureStorage)
|
||||
},
|
||||
HttpClient: &http.Client{},
|
||||
GitClient: f.GitClient,
|
||||
|
|
@ -80,6 +85,7 @@ func NewCmdRefresh(f *cmdutil.Factory, runF func(*RefreshOptions) error) *cobra.
|
|||
|
||||
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().BoolVarP(&opts.SecureStorage, "secure-storage", "", false, "Save authentication credentials in secure credential store")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -152,7 +158,7 @@ func refreshRun(opts *RefreshOptions) error {
|
|||
additionalScopes = append(additionalScopes, credentialFlow.Scopes()...)
|
||||
}
|
||||
|
||||
if err := opts.AuthFlow(authCfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive); err != nil {
|
||||
if err := opts.AuthFlow(authCfg, opts.IO, hostname, append(opts.Scopes, additionalScopes...), opts.Interactive, opts.SecureStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,14 @@ func Test_NewCmdRefresh(t *testing.T) {
|
|||
Scopes: []string{"repo:invite", "read:public_key"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secure storage",
|
||||
tty: true,
|
||||
cli: "--secure-storage",
|
||||
wants: RefreshOptions{
|
||||
SecureStorage: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -126,8 +134,10 @@ func Test_NewCmdRefresh(t *testing.T) {
|
|||
}
|
||||
|
||||
type authArgs struct {
|
||||
hostname string
|
||||
scopes []string
|
||||
hostname string
|
||||
scopes []string
|
||||
interactive bool
|
||||
secureStorage bool
|
||||
}
|
||||
|
||||
func Test_refreshRun(t *testing.T) {
|
||||
|
|
@ -230,17 +240,33 @@ func Test_refreshRun(t *testing.T) {
|
|||
scopes: []string{"repo:invite", "public_key:read", "delete_repo", "codespace"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "secure storage",
|
||||
cfgHosts: []string{
|
||||
"obed.morton",
|
||||
},
|
||||
opts: &RefreshOptions{
|
||||
Hostname: "obed.morton",
|
||||
SecureStorage: true,
|
||||
},
|
||||
wantAuthArgs: authArgs{
|
||||
hostname: "obed.morton",
|
||||
scopes: nil,
|
||||
secureStorage: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
aa := authArgs{}
|
||||
tt.opts.AuthFlow = func(_ *config.AuthConfig, _ *iostreams.IOStreams, hostname string, scopes []string, interactive bool) error {
|
||||
tt.opts.AuthFlow = func(_ *config.AuthConfig, _ *iostreams.IOStreams, hostname string, scopes []string, interactive, secureStorage bool) error {
|
||||
aa.hostname = hostname
|
||||
aa.scopes = scopes
|
||||
aa.interactive = interactive
|
||||
aa.secureStorage = secureStorage
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = config.StubWriteConfig(t)
|
||||
cfg := config.NewFromString("")
|
||||
for _, hostname := range tt.cfgHosts {
|
||||
cfg.Set(hostname, "oauth_token", "abc123")
|
||||
|
|
@ -291,6 +317,8 @@ func Test_refreshRun(t *testing.T) {
|
|||
|
||||
assert.Equal(t, tt.wantAuthArgs.hostname, aa.hostname)
|
||||
assert.Equal(t, tt.wantAuthArgs.scopes, aa.scopes)
|
||||
assert.Equal(t, tt.wantAuthArgs.interactive, aa.interactive)
|
||||
assert.Equal(t, tt.wantAuthArgs.secureStorage, aa.secureStorage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,18 +24,19 @@ type iconfig interface {
|
|||
}
|
||||
|
||||
type LoginOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config iconfig
|
||||
HTTPClient *http.Client
|
||||
GitClient *git.Client
|
||||
Hostname string
|
||||
Interactive bool
|
||||
Web bool
|
||||
Scopes []string
|
||||
Executable string
|
||||
GitProtocol string
|
||||
Prompter Prompt
|
||||
Browser browser.Browser
|
||||
IO *iostreams.IOStreams
|
||||
Config iconfig
|
||||
HTTPClient *http.Client
|
||||
GitClient *git.Client
|
||||
Hostname string
|
||||
Interactive bool
|
||||
Web bool
|
||||
Scopes []string
|
||||
Executable string
|
||||
GitProtocol string
|
||||
Prompter Prompt
|
||||
Browser browser.Browser
|
||||
SecureStorage bool
|
||||
|
||||
sshContext ssh.Context
|
||||
}
|
||||
|
|
@ -186,7 +187,7 @@ func Login(opts *LoginOptions) error {
|
|||
fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", cs.SuccessIcon())
|
||||
}
|
||||
|
||||
if err := cfg.Login(hostname, username, authToken, gitProtocol, false); err != nil {
|
||||
if err := cfg.Login(hostname, username, authToken, gitProtocol, opts.SecureStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
)
|
||||
"strings"
|
||||
|
||||
const (
|
||||
oauthToken = "oauth_token"
|
||||
"github.com/cli/cli/v2/internal/config"
|
||||
)
|
||||
|
||||
func AuthTokenWriteable(authCfg *config.AuthConfig, hostname string) (string, bool) {
|
||||
token, src := authCfg.Token(hostname)
|
||||
return src, (token == "" || src == oauthToken)
|
||||
return src, (token == "" || !strings.HasSuffix(src, "_TOKEN"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ type TokenOptions struct {
|
|||
IO *iostreams.IOStreams
|
||||
Config func() (config.Config, error)
|
||||
|
||||
Hostname string
|
||||
Hostname string
|
||||
SecureStorage bool
|
||||
}
|
||||
|
||||
func NewCmdToken(f *cmdutil.Factory, runF func(*TokenOptions) error) *cobra.Command {
|
||||
|
|
@ -37,6 +38,8 @@ func NewCmdToken(f *cmdutil.Factory, runF func(*TokenOptions) error) *cobra.Comm
|
|||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "The hostname of the GitHub instance authenticated with")
|
||||
cmd.Flags().BoolVarP(&opts.SecureStorage, "secure-storage", "", false, "Search only secure credential store for authentication token")
|
||||
_ = cmd.Flags().MarkHidden("secure-storeage")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -51,8 +54,14 @@ func tokenRun(opts *TokenOptions) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authCfg := cfg.Authentication()
|
||||
|
||||
val, _ := cfg.AuthToken(hostname)
|
||||
var val string
|
||||
if opts.SecureStorage {
|
||||
val, _ = authCfg.TokenFromKeyring(hostname)
|
||||
} else {
|
||||
val, _ = authCfg.Token(hostname)
|
||||
}
|
||||
if val == "" {
|
||||
return fmt.Errorf("no oauth token")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/cli/cli/v2/pkg/iostreams"
|
||||
"github.com/google/shlex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
func TestNewCmdToken(t *testing.T) {
|
||||
|
|
@ -34,6 +35,11 @@ func TestNewCmdToken(t *testing.T) {
|
|||
input: "-h github.mycompany.com",
|
||||
output: TokenOptions{Hostname: "github.mycompany.com"},
|
||||
},
|
||||
{
|
||||
name: "with secure-storage",
|
||||
input: "--secure-storage",
|
||||
output: TokenOptions{SecureStorage: true},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
@ -71,11 +77,12 @@ func TestNewCmdToken(t *testing.T) {
|
|||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.output.Hostname, cmdOpts.Hostname)
|
||||
assert.Equal(t, tt.output.SecureStorage, cmdOpts.SecureStorage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tokenRun(t *testing.T) {
|
||||
func TestTokenRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts TokenOptions
|
||||
|
|
@ -121,17 +128,77 @@ func Test_tokenRun(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
tt.opts.IO = ios
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
tt.opts.IO = ios
|
||||
err := tokenRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenRunSecureStorage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts TokenOptions
|
||||
wantStdout string
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "token",
|
||||
opts: TokenOptions{
|
||||
Config: func() (config.Config, error) {
|
||||
cfg := config.NewBlankConfig()
|
||||
_ = keyring.Set("gh:github.com", "", "gho_ABCDEFG")
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
wantStdout: "gho_ABCDEFG\n",
|
||||
},
|
||||
{
|
||||
name: "token by hostname",
|
||||
opts: TokenOptions{
|
||||
Config: func() (config.Config, error) {
|
||||
cfg := config.NewBlankConfig()
|
||||
_ = keyring.Set("gh:mycompany.com", "", "gho_1234567")
|
||||
return cfg, nil
|
||||
},
|
||||
Hostname: "mycompany.com",
|
||||
},
|
||||
wantStdout: "gho_1234567\n",
|
||||
},
|
||||
{
|
||||
name: "no token",
|
||||
opts: TokenOptions{
|
||||
Config: func() (config.Config, error) {
|
||||
cfg := config.NewBlankConfig()
|
||||
return cfg, nil
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "no oauth token",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
keyring.MockInit()
|
||||
ios, _, stdout, _ := iostreams.Test()
|
||||
tt.opts.IO = ios
|
||||
tt.opts.SecureStorage = true
|
||||
err := tokenRun(&tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.EqualError(t, err, tt.wantErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantStdout, stdout.String())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -242,16 +242,17 @@ func handleRequests(ctx context.Context, server *Server, channel ssh.Channel, re
|
|||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
for req := range reqs {
|
||||
if req.WantReply {
|
||||
if err := req.Reply(true, nil); err != nil {
|
||||
r := req
|
||||
if r.WantReply {
|
||||
if err := r.Reply(true, nil); err != nil {
|
||||
sendError(errc, fmt.Errorf("error replying to channel request: %w", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Type, "stream-transport") {
|
||||
if strings.HasPrefix(r.Type, "stream-transport") {
|
||||
go func() {
|
||||
if err := forwardStream(ctx, server, req.Type, channel); err != nil {
|
||||
if err := forwardStream(ctx, server, r.Type, channel); err != nil {
|
||||
sendError(errc, fmt.Errorf("failed to forward stream: %w", err))
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue