From 4668810cbe8cd36d6fe5687dabb28950448b3c19 Mon Sep 17 00:00:00 2001 From: Kousik Mitra Date: Wed, 5 Apr 2023 18:54:29 +0530 Subject: [PATCH] Enhance ssh-key list to show signing ssh keys also --- pkg/cmd/ssh-key/list/list.go | 35 ++++++++- pkg/cmd/ssh-key/list/list_test.go | 114 ++++++++++++++++++++++++++-- pkg/cmd/ssh-key/shared/user_keys.go | 9 +++ 3 files changed, 147 insertions(+), 11 deletions(-) diff --git a/pkg/cmd/ssh-key/list/list.go b/pkg/cmd/ssh-key/list/list.go index 0c0cbcd0b..12ee6d635 100644 --- a/pkg/cmd/ssh-key/list/list.go +++ b/pkg/cmd/ssh-key/list/list.go @@ -1,10 +1,14 @@ package list import ( + "errors" + "fmt" + "io" "net/http" "strconv" "time" + "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/config" "github.com/cli/cli/v2/internal/text" "github.com/cli/cli/v2/pkg/cmd/ssh-key/shared" @@ -55,12 +59,22 @@ func listRun(opts *ListOptions) error { } host, _ := cfg.Authentication().DefaultHost() - - sshKeys, err := shared.UserKeys(apiClient, host, "") - if err != nil { - return err + sshAuthKeys, authKeyErr := shared.UserKeys(apiClient, host, "") + if authKeyErr != nil { + printError(opts.IO.ErrOut, authKeyErr) } + sshSigningKeys, signKeyErr := shared.UserSigningKeys(apiClient, host, "") + if signKeyErr != nil { + printError(opts.IO.ErrOut, signKeyErr) + } + + if authKeyErr != nil && signKeyErr != nil { + return cmdutil.SilentError + } + + sshKeys := append(sshAuthKeys, sshSigningKeys...) + if len(sshKeys) == 0 { return cmdutil.NewNoResultsError("no SSH keys present in the GitHub account") } @@ -74,6 +88,7 @@ func listRun(opts *ListOptions) error { t.AddField("TITLE", nil, nil) t.AddField("ID", nil, nil) t.AddField("KEY", nil, nil) + t.AddField("TYPE", nil, nil) t.AddField("ADDED", nil, nil) t.EndRow() } @@ -86,12 +101,14 @@ func listRun(opts *ListOptions) error { t.AddField(sshKey.Title, nil, nil) t.AddField(id, nil, nil) t.AddField(sshKey.Key, truncateMiddle, nil) + t.AddField(sshKey.Type, nil, nil) t.AddField(text.FuzzyAgoAbbr(now, sshKey.CreatedAt), nil, cs.Gray) } else { t.AddField(sshKey.Title, nil, nil) t.AddField(sshKey.Key, nil, nil) t.AddField(createdAt, nil, nil) t.AddField(id, nil, nil) + t.AddField(sshKey.Type, nil, nil) } t.EndRow() @@ -114,3 +131,13 @@ func truncateMiddle(maxWidth int, t string) string { remainder := (maxWidth - len(ellipsis)) % 2 return t[0:halfWidth+remainder] + ellipsis + t[len(t)-halfWidth:] } + +func printError(w io.Writer, err error) { + fmt.Fprintln(w, "warning: ", err) + var httpErr api.HTTPError + if errors.As(err, &httpErr) { + if msg := httpErr.ScopesSuggestion(); msg != "" { + fmt.Fprintln(w, msg) + } + } +} diff --git a/pkg/cmd/ssh-key/list/list_test.go b/pkg/cmd/ssh-key/list/list_test.go index 91466ad8f..7334beccf 100644 --- a/pkg/cmd/ssh-key/list/list_test.go +++ b/pkg/cmd/ssh-key/list/list_test.go @@ -22,7 +22,7 @@ func TestListRun(t *testing.T) { wantErr bool }{ { - name: "list tty", + name: "list authentication and signing keys; in tty", opts: ListOptions{ HTTPClient: func() (*http.Client, error) { createdAt := time.Now().Add(time.Duration(-24) * time.Hour) @@ -44,19 +44,31 @@ func TestListRun(t *testing.T) { } ]`, createdAt.Format(time.RFC3339))), ) + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StringResponse(fmt.Sprintf(`[ + { + "id": 321, + "key": "ssh-rsa AAAABbBB123", + "title": "Mac Signing", + "created_at": "%[1]s" + } + ]`, createdAt.Format(time.RFC3339))), + ) return &http.Client{Transport: reg}, nil }, }, isTTY: true, wantStdout: heredoc.Doc(` - TITLE ID KEY ADDED - Mac 1234 ssh-rsa AAAABbBB123 1d - hubot@Windows 5678 ssh-rsa EEEEEEEK247 1d + TITLE ID KEY TYPE ADDED + Mac 1234 ssh-rsa AAAABbBB123 authentication 1d + hubot@Windows 5678 ssh-rsa EEEEEEEK247 authentication 1d + Mac Signing 321 ssh-rsa AAAABbBB123 signing 1d `), wantStderr: "", }, { - name: "list non-tty", + name: "list authentication and signing keys; in non-tty", opts: ListOptions{ HTTPClient: func() (*http.Client, error) { createdAt, _ := time.Parse(time.RFC3339, "2020-08-31T15:44:24+02:00") @@ -78,16 +90,96 @@ func TestListRun(t *testing.T) { } ]`, createdAt.Format(time.RFC3339))), ) + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StringResponse(fmt.Sprintf(`[ + { + "id": 321, + "key": "ssh-rsa AAAABbBB123", + "title": "Mac Signing", + "created_at": "%[1]s" + } + ]`, createdAt.Format(time.RFC3339))), + ) return &http.Client{Transport: reg}, nil }, }, isTTY: false, wantStdout: heredoc.Doc(` - Mac ssh-rsa AAAABbBB123 2020-08-31T15:44:24+02:00 1234 - hubot@Windows ssh-rsa EEEEEEEK247 2020-08-31T15:44:24+02:00 5678 + Mac ssh-rsa AAAABbBB123 2020-08-31T15:44:24+02:00 1234 authentication + hubot@Windows ssh-rsa EEEEEEEK247 2020-08-31T15:44:24+02:00 5678 authentication + Mac Signing ssh-rsa AAAABbBB123 2020-08-31T15:44:24+02:00 321 signing `), wantStderr: "", }, + { + name: "only authentication ssh keys are available", + opts: ListOptions{ + HTTPClient: func() (*http.Client, error) { + createdAt := time.Now().Add(time.Duration(-24) * time.Hour) + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StringResponse(fmt.Sprintf(`[ + { + "id": 1234, + "key": "ssh-rsa AAAABbBB123", + "title": "Mac", + "created_at": "%[1]s" + } + ]`, createdAt.Format(time.RFC3339))), + ) + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StatusStringResponse(404, "Not Found"), + ) + return &http.Client{Transport: reg}, nil + }, + }, + wantStdout: heredoc.Doc(` + TITLE ID KEY TYPE ADDED + Mac 1234 ssh-rsa AAAABbBB123 authentication 1d + `), + wantStderr: heredoc.Doc(` + warning: HTTP 404 (https://api.github.com/user/ssh_signing_keys?per_page=100) + `), + wantErr: false, + isTTY: true, + }, + { + name: "only signing ssh keys are available", + opts: ListOptions{ + HTTPClient: func() (*http.Client, error) { + createdAt := time.Now().Add(time.Duration(-24) * time.Hour) + reg := &httpmock.Registry{} + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StringResponse(fmt.Sprintf(`[ + { + "id": 1234, + "key": "ssh-rsa AAAABbBB123", + "title": "Mac", + "created_at": "%[1]s" + } + ]`, createdAt.Format(time.RFC3339))), + ) + reg.Register( + httpmock.REST("GET", "user/keys"), + httpmock.StatusStringResponse(404, "Not Found"), + ) + return &http.Client{Transport: reg}, nil + }, + }, + wantStdout: heredoc.Doc(` + TITLE ID KEY TYPE ADDED + Mac 1234 ssh-rsa AAAABbBB123 signing 1d + `), + wantStderr: heredoc.Doc(` + warning: HTTP 404 (https://api.github.com/user/keys?per_page=100) + `), + wantErr: false, + isTTY: true, + }, { name: "no keys tty", opts: ListOptions{ @@ -97,6 +189,10 @@ func TestListRun(t *testing.T) { httpmock.REST("GET", "user/keys"), httpmock.StringResponse(`[]`), ) + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StringResponse(`[]`), + ) return &http.Client{Transport: reg}, nil }, }, @@ -114,6 +210,10 @@ func TestListRun(t *testing.T) { httpmock.REST("GET", "user/keys"), httpmock.StringResponse(`[]`), ) + reg.Register( + httpmock.REST("GET", "user/ssh_signing_keys"), + httpmock.StringResponse(`[]`), + ) return &http.Client{Transport: reg}, nil }, }, diff --git a/pkg/cmd/ssh-key/shared/user_keys.go b/pkg/cmd/ssh-key/shared/user_keys.go index 299d872c5..035d00244 100644 --- a/pkg/cmd/ssh-key/shared/user_keys.go +++ b/pkg/cmd/ssh-key/shared/user_keys.go @@ -20,6 +20,7 @@ type sshKey struct { ID int Key string Title string + Type string CreatedAt time.Time `json:"created_at"` } @@ -36,6 +37,10 @@ func UserKeys(httpClient *http.Client, host, userHandle string) ([]sshKey, error return nil, err } + for i := 0; i < len(keys); i++ { + keys[i].Type = AuthenticationKey + } + return keys, nil } @@ -52,6 +57,10 @@ func UserSigningKeys(httpClient *http.Client, host, userHandle string) ([]sshKey return nil, err } + for i := 0; i < len(keys); i++ { + keys[i].Type = SigningKey + } + return keys, nil }