X Tutup
Skip to content

Commit e26a1b9

Browse files
edualbmislav
authored andcommitted
add ssh-key command
1 parent b5366c6 commit e26a1b9

File tree

8 files changed

+490
-22
lines changed

8 files changed

+490
-22
lines changed

api/client.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,10 @@ func (c Client) HasMinimumScopes(hostname string) error {
203203
}
204204

205205
search := map[string]bool{
206-
"repo": false,
207-
"read:org": false,
208-
"admin:org": false,
206+
"repo": false,
207+
"read:org": false,
208+
"admin:org": false,
209+
"read:public_key": false,
209210
}
210211
for _, s := range strings.Split(scopesHeader, ",") {
211212
search[strings.TrimSpace(s)] = true
@@ -220,6 +221,10 @@ func (c Client) HasMinimumScopes(hostname string) error {
220221
missingScopes = append(missingScopes, "read:org")
221222
}
222223

224+
if !search["read:public_key"] && !search["admin:public_key"] {
225+
missingScopes = append(missingScopes, "read:public_key")
226+
}
227+
223228
if len(missingScopes) > 0 {
224229
return &MissingScopesError{MissingScopes: missingScopes}
225230
}

internal/authflow/flow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
6565
httpClient.Transport = api.VerboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport)
6666
}
6767

68-
minimumScopes := []string{"repo", "read:org", "gist", "workflow"}
68+
minimumScopes := []string{"repo", "read:org", "gist", "workflow", "read:public_key"}
6969
scopes := append(minimumScopes, additionalScopes...)
7070

7171
callbackURI := "http://127.0.0.1/callback"

pkg/cmd/auth/login/login_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ func Test_loginRun_nontty(t *testing.T) {
210210
Token: "abc123",
211211
},
212212
httpStubs: func(reg *httpmock.Registry) {
213-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org"))
213+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
214214
},
215215
wantHosts: "albert.wesker:\n oauth_token: abc123\n",
216216
},
@@ -221,7 +221,7 @@ func Test_loginRun_nontty(t *testing.T) {
221221
Token: "abc456",
222222
},
223223
httpStubs: func(reg *httpmock.Registry) {
224-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org"))
224+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org,read:public_key"))
225225
},
226226
wantErr: `could not validate token: missing required scope 'repo'`,
227227
},
@@ -243,7 +243,7 @@ func Test_loginRun_nontty(t *testing.T) {
243243
Token: "abc456",
244244
},
245245
httpStubs: func(reg *httpmock.Registry) {
246-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org"))
246+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org,read:public_key"))
247247
},
248248
wantHosts: "github.com:\n oauth_token: abc456\n",
249249
},
@@ -274,7 +274,7 @@ func Test_loginRun_nontty(t *testing.T) {
274274
if tt.httpStubs != nil {
275275
tt.httpStubs(reg)
276276
} else {
277-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org"))
277+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
278278
}
279279

280280
mainBuf := bytes.Buffer{}
@@ -315,7 +315,7 @@ func Test_loginRun_Survey(t *testing.T) {
315315
_ = cfg.Set("github.com", "oauth_token", "ghi789")
316316
},
317317
httpStubs: func(reg *httpmock.Registry) {
318-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
318+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
319319
reg.Register(
320320
httpmock.GraphQL(`query UserCurrent\b`),
321321
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
@@ -341,7 +341,7 @@ func Test_loginRun_Survey(t *testing.T) {
341341
as.StubOne(false) // cache credentials
342342
},
343343
httpStubs: func(reg *httpmock.Registry) {
344-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
344+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
345345
reg.Register(
346346
httpmock.GraphQL(`query UserCurrent\b`),
347347
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
@@ -363,7 +363,7 @@ func Test_loginRun_Survey(t *testing.T) {
363363
as.StubOne(false) // cache credentials
364364
},
365365
httpStubs: func(reg *httpmock.Registry) {
366-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
366+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
367367
reg.Register(
368368
httpmock.GraphQL(`query UserCurrent\b`),
369369
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))
@@ -436,7 +436,7 @@ func Test_loginRun_Survey(t *testing.T) {
436436
if tt.httpStubs != nil {
437437
tt.httpStubs(reg)
438438
} else {
439-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
439+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
440440
reg.Register(
441441
httpmock.GraphQL(`query UserCurrent\b`),
442442
httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`))

pkg/cmd/auth/status/status_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func Test_statusRun(t *testing.T) {
9191
_ = c.Set("github.com", "oauth_token", "abc123")
9292
},
9393
httpStubs: func(reg *httpmock.Registry) {
94-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
94+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
9595
reg.Register(
9696
httpmock.GraphQL(`query UserCurrent\b`),
9797
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
@@ -106,8 +106,8 @@ func Test_statusRun(t *testing.T) {
106106
_ = c.Set("github.com", "oauth_token", "abc123")
107107
},
108108
httpStubs: func(reg *httpmock.Registry) {
109-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,"))
110-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
109+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo"))
110+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
111111
reg.Register(
112112
httpmock.GraphQL(`query UserCurrent\b`),
113113
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
@@ -124,7 +124,7 @@ func Test_statusRun(t *testing.T) {
124124
},
125125
httpStubs: func(reg *httpmock.Registry) {
126126
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno"))
127-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
127+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
128128
reg.Register(
129129
httpmock.GraphQL(`query UserCurrent\b`),
130130
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
@@ -140,8 +140,8 @@ func Test_statusRun(t *testing.T) {
140140
_ = c.Set("github.com", "oauth_token", "abc123")
141141
},
142142
httpStubs: func(reg *httpmock.Registry) {
143-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
144-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
143+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
144+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
145145
reg.Register(
146146
httpmock.GraphQL(`query UserCurrent\b`),
147147
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
@@ -159,8 +159,8 @@ func Test_statusRun(t *testing.T) {
159159
_ = c.Set("github.com", "oauth_token", "xyz456")
160160
},
161161
httpStubs: func(reg *httpmock.Registry) {
162-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
163-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
162+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
163+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
164164
reg.Register(
165165
httpmock.GraphQL(`query UserCurrent\b`),
166166
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))
@@ -180,8 +180,8 @@ func Test_statusRun(t *testing.T) {
180180
_ = c.Set("github.com", "oauth_token", "xyz456")
181181
},
182182
httpStubs: func(reg *httpmock.Registry) {
183-
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,"))
184-
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,"))
183+
reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,read:public_key"))
184+
reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,read:public_key"))
185185
reg.Register(
186186
httpmock.GraphQL(`query UserCurrent\b`),
187187
httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`))

pkg/cmd/root/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
repoCmd "github.com/cli/cli/pkg/cmd/repo"
2121
creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits"
2222
secretCmd "github.com/cli/cli/pkg/cmd/secret"
23+
sshKeyCmd "github.com/cli/cli/pkg/cmd/ssh-key"
2324
versionCmd "github.com/cli/cli/pkg/cmd/version"
2425
"github.com/cli/cli/pkg/cmdutil"
2526
"github.com/spf13/cobra"
@@ -76,6 +77,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
7677
cmd.AddCommand(gistCmd.NewCmdGist(f))
7778
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
7879
cmd.AddCommand(secretCmd.NewCmdSecret(f))
80+
cmd.AddCommand(sshKeyCmd.NewCmdSSHKey(f))
7981

8082
// the `api` command should not inherit any extra HTTP headers
8183
bareHTTPCmdFactory := *f

pkg/cmd/ssh-key/list/list.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package list
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
"github.com/cli/cli/internal/ghinstance"
10+
"github.com/cli/cli/utils"
11+
12+
"github.com/MakeNowJust/heredoc"
13+
"github.com/cli/cli/api"
14+
"github.com/cli/cli/internal/config"
15+
"github.com/cli/cli/pkg/cmdutil"
16+
"github.com/cli/cli/pkg/iostreams"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
// ListOptions struct for list command
21+
type ListOptions struct {
22+
HTTPClient func() (*http.Client, error)
23+
IO *iostreams.IOStreams
24+
Config func() (config.Config, error)
25+
26+
ListMsg []string
27+
}
28+
29+
// NewCmdList creates a command for list all SSH Keys
30+
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
31+
opts := &ListOptions{
32+
HTTPClient: f.HttpClient,
33+
IO: f.IOStreams,
34+
Config: f.Config,
35+
36+
ListMsg: []string{},
37+
}
38+
39+
cmd := &cobra.Command{
40+
Use: "list",
41+
Args: cobra.ExactArgs(0),
42+
Short: "Lists currently added ssh keys",
43+
Long: heredoc.Doc(`Lists currently added ssh keys.
44+
45+
This interactive command lists all SSH keys associated with your account
46+
`),
47+
Example: heredoc.Doc(`
48+
$ gh ssh-key list
49+
# => lists all ssh keys associated with your account
50+
`),
51+
RunE: func(cmd *cobra.Command, args []string) error {
52+
if runF != nil {
53+
return runF(opts)
54+
}
55+
return listRun(opts)
56+
},
57+
}
58+
59+
return cmd
60+
}
61+
62+
func listRun(opts *ListOptions) error {
63+
apiClient, err := opts.getAPIClient()
64+
if err != nil {
65+
opts.printTerminal()
66+
return err
67+
}
68+
69+
err = opts.hasMinimumScopes(apiClient)
70+
if err != nil {
71+
opts.printTerminal()
72+
return err
73+
}
74+
75+
type keys struct {
76+
Title string
77+
Key string
78+
}
79+
80+
type result []keys
81+
82+
rs := result{}
83+
body := bytes.NewBufferString("")
84+
85+
err = apiClient.REST(ghinstance.Default(), "GET", "user/keys", body, &rs)
86+
if err != nil {
87+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: Got %s", utils.RedX(), err))
88+
opts.printTerminal()
89+
return err
90+
}
91+
92+
for _, r := range rs {
93+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s %s: %s \n %s: %s", utils.Cyan("✹"), utils.Bold("Name"), r.Title, utils.Bold("SSH-KEY"), r.Key))
94+
}
95+
96+
opts.printTerminal()
97+
98+
return nil
99+
}
100+
101+
func (opts *ListOptions) getAPIClient() (*api.Client, error) {
102+
httpClient, err := opts.HTTPClient()
103+
if err != nil {
104+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err))
105+
return nil, err
106+
}
107+
return api.NewClientFromHTTP(httpClient), nil
108+
}
109+
110+
func (opts *ListOptions) hasMinimumScopes(apiClient *api.Client) error {
111+
cfg, err := opts.Config()
112+
if err != nil {
113+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err))
114+
return err
115+
}
116+
117+
hostname := ghinstance.Default()
118+
119+
_, tokenSource, _ := cfg.GetWithSource(hostname, "oauth_token")
120+
121+
// TODO: Implement tests for this case when CheckWriteable function checks filesystem permissions
122+
tokenIsWriteable := cfg.CheckWriteable(hostname, "oauth_token") == nil
123+
124+
err = apiClient.HasMinimumScopes(hostname)
125+
126+
if err != nil {
127+
var missingScopes *api.MissingScopesError
128+
if errors.As(err, &missingScopes) {
129+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: %s", utils.RedX(), err))
130+
if tokenIsWriteable {
131+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To request missing scopes, run: %s %s", utils.Bold("gh auth refresh -h"), hostname))
132+
}
133+
} else {
134+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("%s: authentication failed", utils.RedX()))
135+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- The %s token in %s is no longer valid.", utils.Bold(hostname), utils.Bold(tokenSource)))
136+
if tokenIsWriteable {
137+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To re-authenticate, run: %s %s", utils.Bold("gh auth login -h"), utils.Bold(hostname)))
138+
opts.ListMsg = append(opts.ListMsg, fmt.Sprintf("- To forget about this host, run: %s %s", utils.Bold("gh auth logout -h"), utils.Bold(hostname)))
139+
}
140+
}
141+
return err
142+
}
143+
144+
return nil
145+
}
146+
147+
func (opts *ListOptions) printTerminal() {
148+
stderr := opts.IO.ErrOut
149+
for _, line := range opts.ListMsg {
150+
fmt.Fprintf(stderr, " %s\n", line)
151+
}
152+
}

0 commit comments

Comments
 (0)
X Tutup