Skip to content

Commit 0eecd50

Browse files
committed
Merge remote-tracking branch 'origin/main' into peterguy/clean-up-config
Amp-Thread-ID: https://ampcode.com/threads/T-019cd85e-222f-769f-9950-b0ed936a38dd Co-authored-by: Amp <amp@ampcode.com> # Conflicts: # cmd/src/login.go # cmd/src/login_oauth.go # cmd/src/login_test.go # cmd/src/login_validate.go # cmd/src/main.go # cmd/src/main_test.go # internal/api/api.go
2 parents e62a619 + 4c3f356 commit 0eecd50

File tree

8 files changed

+180
-99
lines changed

8 files changed

+180
-99
lines changed

cmd/src/login.go

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,9 @@ Examples:
2929
3030
$ src login https://sourcegraph.com
3131
32-
Use OAuth device flow to authenticate:
32+
If no access token is configured, 'src login' uses OAuth device flow automatically:
3333
34-
$ src login --oauth https://sourcegraph.com
35-
36-
37-
Override the default client id used during device flow when authenticating:
38-
39-
$ src login --oauth https://sourcegraph.com
34+
$ src login https://sourcegraph.com
4035
`
4136

4237
flagSet := flag.NewFlagSet("login", flag.ExitOnError)
@@ -47,7 +42,6 @@ Examples:
4742

4843
var (
4944
apiFlags = api.NewFlags(flagSet)
50-
useOAuth = flagSet.Bool("oauth", false, "Use OAuth device flow to obtain an access token interactively")
5145
)
5246

5347
handler := func(args []string) error {
@@ -72,7 +66,6 @@ Examples:
7266
cfg: cfg,
7367
client: client,
7468
out: os.Stdout,
75-
useOAuth: *useOAuth,
7669
apiFlags: apiFlags,
7770
oauthClient: oauth.NewClient(oauth.DefaultClientID),
7871
})
@@ -89,7 +82,6 @@ type loginParams struct {
8982
cfg *config
9083
client api.Client
9184
out io.Writer
92-
useOAuth bool
9385
apiFlags *api.Flags
9486
oauthClient oauth.Client
9587
}
@@ -104,41 +96,26 @@ const (
10496
loginFlowValidate
10597
)
10698

107-
var loadStoredOAuthToken = oauth.LoadToken
108-
10999
func loginCmd(ctx context.Context, p loginParams) error {
110100
if p.cfg.configFilePath != "" {
111101
fmt.Fprintln(p.out)
112102
fmt.Fprintf(p.out, "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove %s. See https://github.com/sourcegraph/src-cli#readme for more information.\n", p.cfg.configFilePath)
113103
}
114104

115-
_, flow := selectLoginFlow(ctx, p)
105+
_, flow := selectLoginFlow(p)
116106
return flow(ctx, p)
117107
}
118108

119-
// selectLoginFlow decides what login flow to run based on flags and config.
120-
func selectLoginFlow(ctx context.Context, p loginParams) (loginFlowKind, loginFlow) {
121-
if p.useOAuth {
109+
// selectLoginFlow decides what login flow to run based on configured AuthMode.
110+
func selectLoginFlow(p loginParams) (loginFlowKind, loginFlow) {
111+
switch p.cfg.AuthMode() {
112+
case AuthModeOAuth:
122113
return loginFlowOAuth, runOAuthLogin
123-
}
124-
if !hasEffectiveAuth(ctx, p.cfg) {
114+
case AuthModeAccessToken:
115+
return loginFlowValidate, runValidatedLogin
116+
default:
125117
return loginFlowMissingAuth, runMissingAuthLogin
126118
}
127-
return loginFlowValidate, runValidatedLogin
128-
}
129-
130-
// hasEffectiveAuth determines whether we have auth credentials to continue. It first checks for a resolved Access Token in
131-
// config, then it checks for a stored OAuth token.
132-
func hasEffectiveAuth(ctx context.Context, cfg *config) bool {
133-
if cfg.accessToken != "" {
134-
return true
135-
}
136-
137-
if _, err := loadStoredOAuthToken(ctx, cfg.endpointURL.String()); err == nil {
138-
return true
139-
}
140-
141-
return false
142119
}
143120

144121
func printLoginProblem(out io.Writer, problem string) {
@@ -153,6 +130,6 @@ func loginAccessTokenMessage(endpoint string) string {
153130
154131
To verify that it's working, run the login command again.
155132
156-
Alternatively, you can try logging in using OAuth by running: src login --oauth %s
133+
Alternatively, you can try logging in interactively by running: src login %s
157134
`, endpoint, endpoint, endpoint)
158135
}

cmd/src/login_oauth.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"github.com/sourcegraph/src-cli/internal/oauth"
1414
)
1515

16+
var loadStoredOAuthToken = oauth.LoadToken
17+
1618
func runOAuthLogin(ctx context.Context, p loginParams) error {
1719
endpoint := p.cfg.endpointURL.String()
1820
client, err := oauthLoginClient(ctx, p, endpoint)
@@ -32,7 +34,15 @@ func runOAuthLogin(ctx context.Context, p loginParams) error {
3234
return nil
3335
}
3436

37+
// oauthLoginClient returns a api.Client with the OAuth token set. It will check secret storage for a token
38+
// and use it if one is present.
39+
// If no token is found, it will start a OAuth Device flow to get a token and storage in secret storage.
3540
func oauthLoginClient(ctx context.Context, p loginParams, endpoint string) (api.Client, error) {
41+
// if we have a stored token, used it. Otherwise run the device flow
42+
if token, err := loadStoredOAuthToken(ctx, endpoint); err == nil {
43+
return newOAuthAPIClient(p, endpoint, token), nil
44+
}
45+
3646
token, err := runOAuthDeviceFlow(ctx, endpoint, p.out, p.oauthClient)
3747
if err != nil {
3848
return nil, err
@@ -43,6 +53,10 @@ func oauthLoginClient(ctx context.Context, p loginParams, endpoint string) (api.
4353
fmt.Fprintf(p.out, "⚠️ Warning: Failed to store token in keyring store: %q. Continuing with this session only.\n", err)
4454
}
4555

56+
return newOAuthAPIClient(p, endpoint, token), nil
57+
}
58+
59+
func newOAuthAPIClient(p loginParams, endpoint string, token *oauth.Token) api.Client {
4660
return api.NewClient(api.ClientOpts{
4761
EndpointURL: p.cfg.endpointURL,
4862
AdditionalHeaders: p.cfg.additionalHeaders,
@@ -51,7 +65,7 @@ func oauthLoginClient(ctx context.Context, p loginParams, endpoint string) (api.
5165
ProxyURL: p.cfg.proxyURL,
5266
ProxyPath: p.cfg.proxyPath,
5367
OAuthToken: token,
54-
}), nil
68+
})
5569
}
5670

5771
func runOAuthDeviceFlow(ctx context.Context, endpoint string, out io.Writer, client oauth.Client) (*oauth.Token, error) {

cmd/src/login_test.go

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/url"
1111
"strings"
1212
"testing"
13+
"time"
1314

1415
"github.com/sourcegraph/src-cli/internal/cmderrors"
1516
"github.com/sourcegraph/src-cli/internal/oauth"
@@ -19,16 +20,12 @@ func TestLogin(t *testing.T) {
1920
check := func(t *testing.T, cfg *config) (output string, err error) {
2021
t.Helper()
2122

22-
restoreStoredOAuthLoader(t, func(context.Context, string) (*oauth.Token, error) {
23-
return nil, fmt.Errorf("not found")
24-
})
25-
2623
var out bytes.Buffer
2724
err = loginCmd(context.Background(), loginParams{
2825
cfg: cfg,
2926
client: cfg.apiClient(nil, io.Discard),
3027
out: &out,
31-
oauthClient: oauth.NewClient(oauth.DefaultClientID),
28+
oauthClient: fakeOAuthClient{startErr: fmt.Errorf("oauth unavailable")},
3229
})
3330
return strings.TrimSpace(out.String()), err
3431
}
@@ -39,9 +36,8 @@ func TestLogin(t *testing.T) {
3936
if err != cmderrors.ExitCode1 {
4037
t.Fatal(err)
4138
}
42-
wantOut := "❌ Problem: No access token is configured.\n\n🛠 To fix: Create an access token by going to https://sourcegraph.example.com/user/settings/tokens, then set the following environment variables in your terminal:\n\n export SRC_ENDPOINT=https://sourcegraph.example.com\n export SRC_ACCESS_TOKEN=(your access token)\n\n To verify that it's working, run the login command again.\n\n Alternatively, you can try logging in using OAuth by running: src login --oauth https://sourcegraph.example.com"
43-
if out != wantOut {
44-
t.Errorf("got output %q, want %q", out, wantOut)
39+
if !strings.Contains(out, "OAuth Device flow authentication failed:") {
40+
t.Errorf("got output %q, want oauth failure output", out)
4541
}
4642
})
4743

@@ -51,9 +47,11 @@ func TestLogin(t *testing.T) {
5147
if err != cmderrors.ExitCode1 {
5248
t.Fatal(err)
5349
}
54-
wantOut := "⚠️ Warning: Configuring src with a JSON file is deprecated. Please migrate to using the env vars SRC_ENDPOINT, SRC_ACCESS_TOKEN, and SRC_PROXY instead, and then remove f. See https://github.com/sourcegraph/src-cli#readme for more information.\n\n❌ Problem: No access token is configured.\n\n🛠 To fix: Create an access token by going to https://example.com/user/settings/tokens, then set the following environment variables in your terminal:\n\n export SRC_ENDPOINT=https://example.com\n export SRC_ACCESS_TOKEN=(your access token)\n\n To verify that it's working, run the login command again.\n\n Alternatively, you can try logging in using OAuth by running: src login --oauth https://example.com"
55-
if out != wantOut {
56-
t.Errorf("got output %q, want %q", out, wantOut)
50+
if !strings.Contains(out, "Configuring src with a JSON file is deprecated") {
51+
t.Errorf("got output %q, want deprecation warning", out)
52+
}
53+
if !strings.Contains(out, "OAuth Device flow authentication failed:") {
54+
t.Errorf("got output %q, want oauth failure output", out)
5755
}
5856
})
5957

@@ -68,7 +66,7 @@ func TestLogin(t *testing.T) {
6866
if err != cmderrors.ExitCode1 {
6967
t.Fatal(err)
7068
}
71-
wantOut := "❌ Problem: Invalid access token.\n\n🛠 To fix: Create an access token by going to $ENDPOINT/user/settings/tokens, then set the following environment variables in your terminal:\n\n export SRC_ENDPOINT=$ENDPOINT\n export SRC_ACCESS_TOKEN=(your access token)\n\n To verify that it's working, run the login command again.\n\n Alternatively, you can try logging in using OAuth by running: src login --oauth $ENDPOINT\n\n (If you need to supply custom HTTP request headers, see information about SRC_HEADER_* and SRC_HEADERS env vars at https://github.com/sourcegraph/src-cli/blob/main/AUTH_PROXY.md)"
69+
wantOut := "❌ Problem: Invalid access token.\n\n🛠 To fix: Create an access token by going to $ENDPOINT/user/settings/tokens, then set the following environment variables in your terminal:\n\n export SRC_ENDPOINT=$ENDPOINT\n export SRC_ACCESS_TOKEN=(your access token)\n\n To verify that it's working, run the login command again.\n\n Alternatively, you can try logging in interactively by running: src login $ENDPOINT\n\n (If you need to supply custom HTTP request headers, see information about SRC_HEADER_* and SRC_HEADERS env vars at https://github.com/sourcegraph/src-cli/blob/main/AUTH_PROXY.md)"
7270
wantOut = strings.ReplaceAll(wantOut, "$ENDPOINT", s.URL)
7371
if out != wantOut {
7472
t.Errorf("got output %q, want %q", out, wantOut)
@@ -86,12 +84,81 @@ func TestLogin(t *testing.T) {
8684
if err != nil {
8785
t.Fatal(err)
8886
}
89-
wantOut := "✔ Authenticated as alice on $ENDPOINT"
87+
wantOut := "✔ Authenticated as alice on $ENDPOINT"
9088
wantOut = strings.ReplaceAll(wantOut, "$ENDPOINT", s.URL)
9189
if out != wantOut {
9290
t.Errorf("got output %q, want %q", out, wantOut)
9391
}
9492
})
93+
94+
t.Run("reuses stored oauth token before device flow", func(t *testing.T) {
95+
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
96+
fmt.Fprintln(w, `{"data":{"currentUser":{"username":"alice"}}}`)
97+
}))
98+
defer s.Close()
99+
100+
restoreStoredOAuthLoader(t, func(context.Context, string) (*oauth.Token, error) {
101+
return &oauth.Token{
102+
Endpoint: s.URL,
103+
ClientID: oauth.DefaultClientID,
104+
AccessToken: "oauth-token",
105+
ExpiresAt: time.Now().Add(time.Hour),
106+
}, nil
107+
})
108+
109+
u, _ := url.ParseRequestURI(s.URL)
110+
startCalled := false
111+
var out bytes.Buffer
112+
err := loginCmd(context.Background(), loginParams{
113+
cfg: &config{endpointURL: u},
114+
client: (&config{endpointURL: u}).apiClient(nil, io.Discard),
115+
out: &out,
116+
oauthClient: fakeOAuthClient{
117+
startErr: fmt.Errorf("unexpected call to Start"),
118+
startCalled: &startCalled,
119+
},
120+
})
121+
if err != nil {
122+
t.Fatal(err)
123+
}
124+
if startCalled {
125+
t.Fatal("expected stored oauth token to avoid device flow")
126+
}
127+
gotOut := strings.TrimSpace(out.String())
128+
wantOut := "✔︎ Authenticated as alice on $ENDPOINT\n\n\n✔︎ Authenticated with OAuth credentials"
129+
wantOut = strings.ReplaceAll(wantOut, "$ENDPOINT", s.URL)
130+
if gotOut != wantOut {
131+
t.Errorf("got output %q, want %q", gotOut, wantOut)
132+
}
133+
})
134+
}
135+
136+
type fakeOAuthClient struct {
137+
startErr error
138+
startCalled *bool
139+
}
140+
141+
func (f fakeOAuthClient) ClientID() string {
142+
return oauth.DefaultClientID
143+
}
144+
145+
func (f fakeOAuthClient) Discover(context.Context, string) (*oauth.OIDCConfiguration, error) {
146+
return nil, fmt.Errorf("unexpected call to Discover")
147+
}
148+
149+
func (f fakeOAuthClient) Start(context.Context, string, []string) (*oauth.DeviceAuthResponse, error) {
150+
if f.startCalled != nil {
151+
*f.startCalled = true
152+
}
153+
return nil, f.startErr
154+
}
155+
156+
func (f fakeOAuthClient) Poll(context.Context, string, string, time.Duration, int) (*oauth.TokenResponse, error) {
157+
return nil, fmt.Errorf("unexpected call to Poll")
158+
}
159+
160+
func (f fakeOAuthClient) Refresh(context.Context, *oauth.Token) (*oauth.TokenResponse, error) {
161+
return nil, fmt.Errorf("unexpected call to Refresh")
95162
}
96163

97164
func TestSelectLoginFlow(t *testing.T) {
@@ -103,28 +170,13 @@ func TestSelectLoginFlow(t *testing.T) {
103170
return u
104171
}
105172

106-
restoreStoredOAuthLoader(t, func(context.Context, string) (*oauth.Token, error) {
107-
return nil, fmt.Errorf("not found")
108-
})
109-
110-
t.Run("uses oauth flow when oauth flag is set", func(t *testing.T) {
111-
params := loginParams{
112-
cfg: &config{endpointURL: mustParseURL("https://example.com")},
113-
useOAuth: true,
114-
}
115-
116-
if got, _ := selectLoginFlow(context.Background(), params); got != loginFlowOAuth {
117-
t.Fatalf("flow = %v, want %v", got, loginFlowOAuth)
118-
}
119-
})
120-
121-
t.Run("uses missing auth flow when auth is unavailable", func(t *testing.T) {
173+
t.Run("uses oauth flow when no access token is configured", func(t *testing.T) {
122174
params := loginParams{
123175
cfg: &config{endpointURL: mustParseURL("https://example.com")},
124176
}
125177

126-
if got, _ := selectLoginFlow(context.Background(), params); got != loginFlowMissingAuth {
127-
t.Fatalf("flow = %v, want %v", got, loginFlowMissingAuth)
178+
if got, _ := selectLoginFlow(params); got != loginFlowOAuth {
179+
t.Fatalf("flow = %v, want %v", got, loginFlowOAuth)
128180
}
129181
})
130182

@@ -133,21 +185,7 @@ func TestSelectLoginFlow(t *testing.T) {
133185
cfg: &config{endpointURL: mustParseURL("https://example.com"), accessToken: "x"},
134186
}
135187

136-
if got, _ := selectLoginFlow(context.Background(), params); got != loginFlowValidate {
137-
t.Fatalf("flow = %v, want %v", got, loginFlowValidate)
138-
}
139-
})
140-
141-
t.Run("treats stored oauth as effective auth", func(t *testing.T) {
142-
restoreStoredOAuthLoader(t, func(context.Context, string) (*oauth.Token, error) {
143-
return &oauth.Token{AccessToken: "oauth-token"}, nil
144-
})
145-
146-
params := loginParams{
147-
cfg: &config{endpointURL: mustParseURL("https://example.com")},
148-
}
149-
150-
if got, _ := selectLoginFlow(context.Background(), params); got != loginFlowValidate {
188+
if got, _ := selectLoginFlow(params); got != loginFlowValidate {
151189
t.Fatalf("flow = %v, want %v", got, loginFlowValidate)
152190
}
153191
})

cmd/src/login_validate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func validateCurrentUser(ctx context.Context, client api.Client, out io.Writer,
4646
return cmderrors.ExitCode1
4747
}
4848
fmt.Fprintln(out)
49-
fmt.Fprintf(out, "✔ Authenticated as %s on %s\n", result.CurrentUser.Username, endpoint)
49+
fmt.Fprintf(out, "✔ Authenticated as %s on %s\n", result.CurrentUser.Username, endpoint)
5050
fmt.Fprintln(out)
5151
return nil
5252
}

0 commit comments

Comments
 (0)