X Tutup
Skip to content

Commit edecb2e

Browse files
authored
Merge pull request cli#2207 from cli/codespaces
[Codespaces] Support "integration" tokens
2 parents e74e431 + 35517eb commit edecb2e

File tree

4 files changed

+66
-131
lines changed

4 files changed

+66
-131
lines changed

api/client.go

Lines changed: 26 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package api
33
import (
44
"bytes"
55
"encoding/json"
6-
"errors"
76
"fmt"
87
"io"
98
"io/ioutil"
@@ -99,58 +98,6 @@ func ReplaceTripper(tr http.RoundTripper) ClientOption {
9998
}
10099
}
101100

102-
var issuedScopesWarning bool
103-
104-
const (
105-
httpOAuthAppID = "X-Oauth-Client-Id"
106-
httpOAuthScopes = "X-Oauth-Scopes"
107-
)
108-
109-
// CheckScopes checks whether an OAuth scope is present in a response
110-
func CheckScopes(wantedScope string, cb func(string) error) ClientOption {
111-
wantedCandidates := []string{wantedScope}
112-
if strings.HasPrefix(wantedScope, "read:") {
113-
wantedCandidates = append(wantedCandidates, "admin:"+strings.TrimPrefix(wantedScope, "read:"))
114-
}
115-
116-
return func(tr http.RoundTripper) http.RoundTripper {
117-
return &funcTripper{roundTrip: func(req *http.Request) (*http.Response, error) {
118-
res, err := tr.RoundTrip(req)
119-
if err != nil || res.StatusCode > 299 || issuedScopesWarning {
120-
return res, err
121-
}
122-
123-
_, hasHeader := res.Header[httpOAuthAppID]
124-
if !hasHeader {
125-
return res, nil
126-
}
127-
128-
appID := res.Header.Get(httpOAuthAppID)
129-
hasScopes := strings.Split(res.Header.Get(httpOAuthScopes), ",")
130-
131-
hasWanted := false
132-
outer:
133-
for _, s := range hasScopes {
134-
for _, w := range wantedCandidates {
135-
if w == strings.TrimSpace(s) {
136-
hasWanted = true
137-
break outer
138-
}
139-
}
140-
}
141-
142-
if !hasWanted {
143-
if err := cb(appID); err != nil {
144-
return res, err
145-
}
146-
issuedScopesWarning = true
147-
}
148-
149-
return res, nil
150-
}}
151-
}
152-
}
153-
154101
type funcTripper struct {
155102
roundTrip func(*http.Request) (*http.Response, error)
156103
}
@@ -207,7 +154,20 @@ func (err HTTPError) Error() string {
207154
}
208155

209156
type MissingScopesError struct {
210-
error
157+
MissingScopes []string
158+
}
159+
160+
func (e MissingScopesError) Error() string {
161+
var missing []string
162+
for _, s := range e.MissingScopes {
163+
missing = append(missing, fmt.Sprintf("'%s'", s))
164+
}
165+
scopes := strings.Join(missing, ", ")
166+
167+
if len(e.MissingScopes) == 1 {
168+
return "missing required scope " + scopes
169+
}
170+
return "missing required scopes " + scopes
211171
}
212172

213173
func (c Client) HasMinimumScopes(hostname string) error {
@@ -235,31 +195,34 @@ func (c Client) HasMinimumScopes(hostname string) error {
235195
return HandleHTTPError(res)
236196
}
237197

238-
hasScopes := strings.Split(res.Header.Get("X-Oauth-Scopes"), ",")
198+
scopesHeader := res.Header.Get("X-Oauth-Scopes")
199+
if scopesHeader == "" {
200+
// if the token reports no scopes, assume that it's an integration token and give up on
201+
// detecting its capabilities
202+
return nil
203+
}
239204

240205
search := map[string]bool{
241206
"repo": false,
242207
"read:org": false,
243208
"admin:org": false,
244209
}
245-
246-
for _, s := range hasScopes {
210+
for _, s := range strings.Split(scopesHeader, ",") {
247211
search[strings.TrimSpace(s)] = true
248212
}
249213

250-
errorMsgs := []string{}
214+
var missingScopes []string
251215
if !search["repo"] {
252-
errorMsgs = append(errorMsgs, "missing required scope 'repo'")
216+
missingScopes = append(missingScopes, "repo")
253217
}
254218

255219
if !search["read:org"] && !search["admin:org"] {
256-
errorMsgs = append(errorMsgs, "missing required scope 'read:org'")
220+
missingScopes = append(missingScopes, "read:org")
257221
}
258222

259-
if len(errorMsgs) > 0 {
260-
return &MissingScopesError{error: errors.New(strings.Join(errorMsgs, ";"))}
223+
if len(missingScopes) > 0 {
224+
return &MissingScopesError{MissingScopes: missingScopes}
261225
}
262-
263226
return nil
264227
}
265228

api/client_test.go

Lines changed: 36 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -105,97 +105,66 @@ func TestRESTError(t *testing.T) {
105105
}
106106
}
107107

108-
func Test_CheckScopes(t *testing.T) {
108+
func Test_HasMinimumScopes(t *testing.T) {
109109
tests := []struct {
110-
name string
111-
wantScope string
112-
responseApp string
113-
responseScopes string
114-
responseError error
115-
expectCallback bool
110+
name string
111+
header string
112+
wantErr string
116113
}{
117114
{
118-
name: "missing read:org",
119-
wantScope: "read:org",
120-
responseApp: "APPID",
121-
responseScopes: "repo, gist",
122-
expectCallback: true,
115+
name: "no scopes",
116+
header: "",
117+
wantErr: "",
123118
},
124119
{
125-
name: "has read:org",
126-
wantScope: "read:org",
127-
responseApp: "APPID",
128-
responseScopes: "repo, read:org, gist",
129-
expectCallback: false,
120+
name: "default scopes",
121+
header: "repo, read:org",
122+
wantErr: "",
130123
},
131124
{
132-
name: "has admin:org",
133-
wantScope: "read:org",
134-
responseApp: "APPID",
135-
responseScopes: "repo, admin:org, gist",
136-
expectCallback: false,
125+
name: "admin:org satisfies read:org",
126+
header: "repo, admin:org",
127+
wantErr: "",
137128
},
138129
{
139-
name: "no scopes in response",
140-
wantScope: "read:org",
141-
responseApp: "",
142-
responseScopes: "",
143-
expectCallback: false,
130+
name: "insufficient scope",
131+
header: "repo",
132+
wantErr: "missing required scope 'read:org'",
144133
},
145134
{
146-
name: "errored response",
147-
wantScope: "read:org",
148-
responseApp: "",
149-
responseScopes: "",
150-
responseError: errors.New("Network Failed"),
151-
expectCallback: false,
135+
name: "insufficient scopes",
136+
header: "gist",
137+
wantErr: "missing required scopes 'repo', 'read:org'",
152138
},
153139
}
154140
for _, tt := range tests {
155141
t.Run(tt.name, func(t *testing.T) {
156-
tr := &httpmock.Registry{}
157-
tr.Register(httpmock.MatchAny, func(*http.Request) (*http.Response, error) {
158-
if tt.responseError != nil {
159-
return nil, tt.responseError
160-
}
161-
if tt.responseScopes == "" {
162-
return &http.Response{StatusCode: 200}, nil
163-
}
142+
fakehttp := &httpmock.Registry{}
143+
client := NewClient(ReplaceTripper(fakehttp))
144+
145+
fakehttp.Register(httpmock.REST("GET", ""), func(req *http.Request) (*http.Response, error) {
164146
return &http.Response{
147+
Request: req,
165148
StatusCode: 200,
166-
Header: http.Header{
167-
"X-Oauth-Client-Id": []string{tt.responseApp},
168-
"X-Oauth-Scopes": []string{tt.responseScopes},
149+
Body: ioutil.NopCloser(&bytes.Buffer{}),
150+
Header: map[string][]string{
151+
"X-Oauth-Scopes": {tt.header},
169152
},
170153
}, nil
171154
})
172155

173-
callbackInvoked := false
174-
var gotAppID string
175-
fn := CheckScopes(tt.wantScope, func(appID string) error {
176-
callbackInvoked = true
177-
gotAppID = appID
178-
return nil
179-
})
180-
181-
rt := fn(tr)
182-
req, err := http.NewRequest("GET", "https://api.github.com/hello", nil)
183-
if err != nil {
184-
t.Fatalf("unexpected error: %v", err)
185-
}
186-
187-
issuedScopesWarning = false
188-
_, err = rt.RoundTrip(req)
189-
if err != nil && !errors.Is(err, tt.responseError) {
190-
t.Fatalf("unexpected error: %v", err)
156+
err := client.HasMinimumScopes("github.com")
157+
if tt.wantErr == "" {
158+
if err != nil {
159+
t.Errorf("error: %v", err)
160+
}
161+
return
191162
}
163+
if err.Error() != tt.wantErr {
164+
t.Errorf("want %q, got %q", tt.wantErr, err.Error())
192165

193-
if tt.expectCallback != callbackInvoked {
194-
t.Fatalf("expected CheckScopes callback: %v", tt.expectCallback)
195-
}
196-
if tt.expectCallback && gotAppID != tt.responseApp {
197-
t.Errorf("unexpected app ID: %q", gotAppID)
198166
}
199167
})
200168
}
169+
201170
}

cmd/gh/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ func shouldCheckForUpdate() bool {
191191
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
192192
return false
193193
}
194+
if os.Getenv("CODESPACES") != "" {
195+
return false
196+
}
194197
return updaterEnabled != "" && !isCI() && !isCompletionCommand() && utils.IsTerminal(os.Stderr)
195198
}
196199

pkg/cmd/auth/status/status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func statusRun(opts *StatusOptions) error {
9898
if err != nil {
9999
var missingScopes *api.MissingScopesError
100100
if errors.As(err, &missingScopes) {
101-
addMsg("%s %s: %s", utils.Red("X"), hostname, err)
101+
addMsg("%s %s: the token in %s is %s", utils.Red("X"), hostname, tokenSource, err)
102102
if tokenIsWriteable {
103103
addMsg("- To request missing scopes, run: %s %s\n",
104104
utils.Bold("gh auth refresh -h"),

0 commit comments

Comments
 (0)
X Tutup