Skip to content

Commit

Permalink
Merge pull request #8 from schustafa/check-user-scopes-on-failure
Browse files Browse the repository at this point in the history
Use X-OAuth-Scopes response header to determine missing scopes
  • Loading branch information
schustafa authored Jul 18, 2024
2 parents f0a2866 + 8a56034 commit 9e2b95c
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 2 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/cli/go-gh/v2 v2.9.0
github.com/deckarep/golang-set/v2 v2.6.0
github.com/mergestat/fluentgraphql v0.0.0-20220506205554-9162f392519f
github.com/stretchr/testify v1.7.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
Expand Down
32 changes: 30 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
"io"
"net/http"
"os"
"strings"

"github.com/cli/go-gh/v2/pkg/auth"
mapset "github.com/deckarep/golang-set/v2"
fgql "github.com/mergestat/fluentgraphql"
)

Expand Down Expand Up @@ -77,47 +79,73 @@ func generateQuery(usernames []string) map[string]interface{} {
return body
}

// missingTokenScopes returns a set of scopes that are required but not present
// in the passed string value of the X-OAuth-Scopes header.
func missingTokenScopes(scopesHeader string) mapset.Set[string] {
requiredScopes := mapset.NewSet[string]("user:email", "read:user")
scopesHeader = strings.ReplaceAll(scopesHeader, " ", "")

tokenScopes := mapset.NewSet[string](strings.Split(scopesHeader, ",")...)

return requiredScopes.Difference(tokenScopes)
}

func cli() error {
// Parse handles from command-line arguments and generate a request body
handles := flag.Args()

body := generateQuery(handles)

// Marshal the request body to JSON; return and print error if that fails
b, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("could not marshal body: %w", err)
}

// Build the request; return and print error if that fails
req, err := http.NewRequest(http.MethodPost, "https://api.github.com/graphql", bytes.NewReader(b))
if err != nil {
return fmt.Errorf("could not build request: %w", err)
}

// Update the authorization header with the GitHub token
githubToken, _ := auth.TokenForHost("github.com")
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", githubToken))

// Make the request; return and print error if that fails
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("API call failed: %w", err)
}

// Read the response body; return and print error if that fails
resBody, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("could not read: %w", err)
}

var graphqlResponse map[string]interface{}

// Unmarshal the response body; return and print error if that fails
err = json.Unmarshal(resBody, &graphqlResponse)
if err != nil {
return fmt.Errorf("could not unmarshal: %w", err)
}

data, ok := graphqlResponse["data"].(map[string]interface{})

// If the response body does not contain a "data" key, the token may be
// missing required scopes
if !ok {
return fmt.Errorf("could not parse response.\n\nyou may need to add the appropriate scopes to your token.\ntry running the following:\n\tgh auth refresh --scopes user:email,read:user")
missingScopes := missingTokenScopes(res.Header.Get("X-OAuth-Scopes"))

if missingScopes.Cardinality() > 0 {
return fmt.Errorf("your token is missing required scopes. try running the following:\n\tgh auth refresh --scopes %s", strings.Join(missingScopes.ToSlice(), ","))
}

return fmt.Errorf("could not parse response")
}

// Print a co-authored-by line for each user in the returned data set
for _, user := range data {
if user == nil {
continue
Expand Down
25 changes: 25 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"testing"

mapset "github.com/deckarep/golang-set/v2"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -74,3 +75,27 @@ func TestCoAuthoredByWithEmailAndName(t *testing.T) {
expectedCoAuthoredBy := "Co-authored-by: Miss Mona Lisa Octocat <monalisa@github.com>\n"
assert.Equal(t, expectedCoAuthoredBy, user.coAuthoredBy())
}

func TestMissingScopes(t *testing.T) {
scopesHeader := ""
expectedMissingScopes := mapset.NewSet("user:email", "read:user")
assert.Equal(t, expectedMissingScopes, missingTokenScopes(scopesHeader))
}

func TestMissingScopesOnlyWhitespace(t *testing.T) {
scopesHeader := " "
expectedMissingScopes := mapset.NewSet("user:email", "read:user")
assert.Equal(t, expectedMissingScopes, missingTokenScopes(scopesHeader))
}

func TestMissingScopesWithAllScopes(t *testing.T) {
scopesHeader := " codespace, gist, read:org, read:user, repo, user:email, workflow "
expectedMissingScopes := mapset.NewSet[string]()
assert.Equal(t, expectedMissingScopes, missingTokenScopes(scopesHeader))
}

func TestMissingScopesMissingOneScope(t *testing.T) {
scopesHeader := " codespace, gist, read:org, repo, user:email, workflow "
expectedMissingScopes := mapset.NewSet("read:user")
assert.Equal(t, expectedMissingScopes, missingTokenScopes(scopesHeader))
}

0 comments on commit 9e2b95c

Please sign in to comment.