Skip to content

Commit

Permalink
Merge pull request #2892 from cli/auth-with-ssh
Browse files Browse the repository at this point in the history
Add SSH key generation & uploading to `gh auth login` flow
  • Loading branch information
mislav committed Feb 17, 2021
2 parents e874236 + b4bf8cd commit 3a224b7
Show file tree
Hide file tree
Showing 16 changed files with 831 additions and 445 deletions.
92 changes: 11 additions & 81 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ type HTTPError struct {
RequestURL *url.URL
Message string
OAuthScopes string
Errors []HTTPErrorItem
}

type HTTPErrorItem struct {
Message string
Resource string
Field string
Code string
}

func (err HTTPError) Error() string {
Expand All @@ -153,79 +161,6 @@ func (err HTTPError) Error() string {
return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL)
}

type MissingScopesError struct {
MissingScopes []string
}

func (e MissingScopesError) Error() string {
var missing []string
for _, s := range e.MissingScopes {
missing = append(missing, fmt.Sprintf("'%s'", s))
}
scopes := strings.Join(missing, ", ")

if len(e.MissingScopes) == 1 {
return "missing required scope " + scopes
}
return "missing required scopes " + scopes
}

func (c Client) HasMinimumScopes(hostname string) error {
apiEndpoint := ghinstance.RESTPrefix(hostname)

req, err := http.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return err
}

req.Header.Set("Content-Type", "application/json; charset=utf-8")
res, err := c.http.Do(req)
if err != nil {
return err
}

defer func() {
// Ensure the response body is fully read and closed
// before we reconnect, so that we reuse the same TCPconnection.
_, _ = io.Copy(ioutil.Discard, res.Body)
res.Body.Close()
}()

if res.StatusCode != 200 {
return HandleHTTPError(res)
}

scopesHeader := res.Header.Get("X-Oauth-Scopes")
if scopesHeader == "" {
// if the token reports no scopes, assume that it's an integration token and give up on
// detecting its capabilities
return nil
}

search := map[string]bool{
"repo": false,
"read:org": false,
"admin:org": false,
}
for _, s := range strings.Split(scopesHeader, ",") {
search[strings.TrimSpace(s)] = true
}

var missingScopes []string
if !search["repo"] {
missingScopes = append(missingScopes, "repo")
}

if !search["read:org"] && !search["admin:org"] {
missingScopes = append(missingScopes, "read:org")
}

if len(missingScopes) > 0 {
return &MissingScopesError{MissingScopes: missingScopes}
}
return nil
}

// GraphQL performs a GraphQL request and parses the response
func (c Client) GraphQL(hostname string, query string, variables map[string]interface{}, data interface{}) error {
reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables})
Expand Down Expand Up @@ -341,22 +276,16 @@ func HandleHTTPError(resp *http.Response) error {
return httpError
}

type errorObject struct {
Message string
Resource string
Field string
Code string
}

messages := []string{parsedBody.Message}
for _, raw := range parsedBody.Errors {
switch raw[0] {
case '"':
var errString string
_ = json.Unmarshal(raw, &errString)
messages = append(messages, errString)
httpError.Errors = append(httpError.Errors, HTTPErrorItem{Message: errString})
case '{':
var errInfo errorObject
var errInfo HTTPErrorItem
_ = json.Unmarshal(raw, &errInfo)
msg := errInfo.Message
if errInfo.Code != "custom" {
Expand All @@ -365,6 +294,7 @@ func HandleHTTPError(resp *http.Response) error {
if msg != "" {
messages = append(messages, msg)
}
httpError.Errors = append(httpError.Errors, errInfo)
}
}
httpError.Message = strings.Join(messages, "\n")
Expand Down
64 changes: 0 additions & 64 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,67 +109,3 @@ func TestRESTError(t *testing.T) {

}
}

func Test_HasMinimumScopes(t *testing.T) {
tests := []struct {
name string
header string
wantErr string
}{
{
name: "no scopes",
header: "",
wantErr: "",
},
{
name: "default scopes",
header: "repo, read:org",
wantErr: "",
},
{
name: "admin:org satisfies read:org",
header: "repo, admin:org",
wantErr: "",
},
{
name: "insufficient scope",
header: "repo",
wantErr: "missing required scope 'read:org'",
},
{
name: "insufficient scopes",
header: "gist",
wantErr: "missing required scopes 'repo', 'read:org'",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakehttp := &httpmock.Registry{}
client := NewClient(ReplaceTripper(fakehttp))

fakehttp.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) {
return &http.Response{
Request: req,
StatusCode: 200,
Body: ioutil.NopCloser(&bytes.Buffer{}),
Header: map[string][]string{
"X-Oauth-Scopes": {tt.header},
},
}, nil
})

err := client.HasMinimumScopes("github.com")
if tt.wantErr == "" {
if err != nil {
t.Errorf("error: %v", err)
}
return
}
if err.Error() != tt.wantErr {
t.Errorf("want %q, got %q", tt.wantErr, err.Error())

}
})
}

}
10 changes: 7 additions & 3 deletions internal/authflow/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strings"

"github.com/cli/cli/api"
"github.com/cli/cli/internal/config"
"github.com/cli/cli/internal/ghinstance"
"github.com/cli/cli/pkg/browser"
"github.com/cli/cli/pkg/iostreams"
Expand All @@ -23,7 +22,12 @@ var (
oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b"
)

func AuthFlowWithConfig(cfg config.Config, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) {
type iconfig interface {
Set(string, string, string) error
Write() error
}

func AuthFlowWithConfig(cfg iconfig, IO *iostreams.IOStreams, hostname, notice string, additionalScopes []string) (string, error) {
// TODO this probably shouldn't live in this package. It should probably be in a new package that
// depends on both iostreams and config.
stderr := IO.ErrOut
Expand Down Expand Up @@ -65,7 +69,7 @@ func authFlow(oauthHost string, IO *iostreams.IOStreams, notice string, addition
httpClient.Transport = api.VerboseLog(IO.ErrOut, logTraffic, IO.ColorEnabled())(httpClient.Transport)
}

minimumScopes := []string{"repo", "read:org", "gist", "workflow"}
minimumScopes := []string{"repo", "read:org", "gist"}
scopes := append(minimumScopes, additionalScopes...)

callbackURI := "http://127.0.0.1/callback"
Expand Down

0 comments on commit 3a224b7

Please sign in to comment.