Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 34 additions & 18 deletions cmd/src/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ func loginCmd(ctx context.Context, p loginParams) error {
export SRC_ACCESS_TOKEN=(your access token)
To verify that it's working, run the login command again.
`, endpointArg, endpointArg)
Alternatively, you can try logging in using OAuth by running: src login --oauth %s
`, endpointArg, endpointArg, endpointArg)

if cfg.ConfigFilePath != "" {
fmt.Fprintln(out)
Expand All @@ -121,6 +123,17 @@ func loginCmd(ctx context.Context, p loginParams) error {

noToken := cfg.AccessToken == ""
endpointConflict := endpointArg != cfg.Endpoint
if !p.useOAuth && (noToken || endpointConflict) {
fmt.Fprintln(out)
switch {
case noToken:
printProblem("No access token is configured.")
case endpointConflict:
printProblem(fmt.Sprintf("The configured endpoint is %s, not %s.", cfg.Endpoint, endpointArg))
}
fmt.Fprintln(out, createAccessTokenMessage)
return cmderrors.ExitCode1
}

if p.useOAuth {
token, err := runOAuthDeviceFlow(ctx, endpointArg, out, p.deviceFlowClient)
Expand All @@ -130,19 +143,20 @@ func loginCmd(ctx context.Context, p loginParams) error {
return cmderrors.ExitCode1
}

cfg.AccessToken = token
cfg.Endpoint = endpointArg
client = cfg.apiClient(p.apiFlags, out)
} else if noToken || endpointConflict {
fmt.Fprintln(out)
switch {
case noToken:
printProblem("No access token is configured.")
case endpointConflict:
printProblem(fmt.Sprintf("The configured endpoint is %s, not %s.", cfg.Endpoint, endpointArg))
if err := oauth.StoreToken(ctx, token); err != nil {
fmt.Fprintln(out)
fmt.Fprintf(out, "⚠️ Warning: Failed to store token in keyring store: %q. Continuing with this session only.\n", err)
}
fmt.Fprintln(out, createAccessTokenMessage)
return cmderrors.ExitCode1

client = api.NewClient(api.ClientOpts{
Endpoint: cfg.Endpoint,
AdditionalHeaders: cfg.AdditionalHeaders,
Flags: p.apiFlags,
Out: out,
ProxyURL: cfg.ProxyURL,
ProxyPath: cfg.ProxyPath,
OAuthToken: token,
})
}

// See if the user is already authenticated.
Expand Down Expand Up @@ -179,10 +193,10 @@ func loginCmd(ctx context.Context, p loginParams) error {
return nil
}

func runOAuthDeviceFlow(ctx context.Context, endpoint string, out io.Writer, client oauth.Client) (string, error) {
func runOAuthDeviceFlow(ctx context.Context, endpoint string, out io.Writer, client oauth.Client) (*oauth.Token, error) {
authResp, err := client.Start(ctx, endpoint, nil)
if err != nil {
return "", err
return nil, err
}

authURL := authResp.VerificationURIComplete
Expand All @@ -204,12 +218,14 @@ func runOAuthDeviceFlow(ctx context.Context, endpoint string, out io.Writer, cli
interval = 5 * time.Second
}

tokenResp, err := client.Poll(ctx, endpoint, authResp.DeviceCode, interval, authResp.ExpiresIn)
resp, err := client.Poll(ctx, endpoint, authResp.DeviceCode, interval, authResp.ExpiresIn)
if err != nil {
return "", err
return nil, err
}

return tokenResp.AccessToken, nil
token := resp.Token(endpoint)
token.ClientID = client.ClientID()
return token, nil
}

func openInBrowser(url string) error {
Expand Down
17 changes: 12 additions & 5 deletions cmd/src/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ import (
"testing"

"github.com/sourcegraph/src-cli/internal/cmderrors"
"github.com/sourcegraph/src-cli/internal/oauth"
)

func TestLogin(t *testing.T) {
check := func(t *testing.T, cfg *config, endpointArg string) (output string, err error) {
t.Helper()

var out bytes.Buffer
err = loginCmd(context.Background(), loginParams{cfg: cfg, client: cfg.apiClient(nil, io.Discard), endpoint: endpointArg, out: &out})
err = loginCmd(context.Background(), loginParams{
cfg: cfg,
client: cfg.apiClient(nil, io.Discard),
endpoint: endpointArg,
out: &out,
deviceFlowClient: oauth.NewClient(oauth.DefaultClientID),
})
return strings.TrimSpace(out.String()), err
}

Expand All @@ -27,7 +34,7 @@ func TestLogin(t *testing.T) {
if err != cmderrors.ExitCode1 {
t.Fatal(err)
}
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."
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"
if out != wantOut {
t.Errorf("got output %q, want %q", out, wantOut)
}
Expand All @@ -38,7 +45,7 @@ func TestLogin(t *testing.T) {
if err != cmderrors.ExitCode1 {
t.Fatal(err)
}
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."
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"
if out != wantOut {
t.Errorf("got output %q, want %q", out, wantOut)
}
Expand All @@ -49,7 +56,7 @@ func TestLogin(t *testing.T) {
if err != cmderrors.ExitCode1 {
t.Fatal(err)
}
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."
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"
if out != wantOut {
t.Errorf("got output %q, want %q", out, wantOut)
}
Expand All @@ -67,7 +74,7 @@ func TestLogin(t *testing.T) {
if err != cmderrors.ExitCode1 {
t.Fatal(err)
}
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 (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)"
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)"
wantOut = strings.ReplaceAll(wantOut, "$ENDPOINT", endpoint)
if out != wantOut {
t.Errorf("got output %q, want %q", out, wantOut)
Expand Down
15 changes: 13 additions & 2 deletions cmd/src/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"encoding/json"
"flag"
"io"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/sourcegraph/sourcegraph/lib/errors"

"github.com/sourcegraph/src-cli/internal/api"
"github.com/sourcegraph/src-cli/internal/oauth"
)

const usageText = `src is a tool that provides access to Sourcegraph instances.
Expand Down Expand Up @@ -122,15 +124,24 @@ type config struct {

// apiClient returns an api.Client built from the configuration.
func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client {
return api.NewClient(api.ClientOpts{
opts := api.ClientOpts{
Endpoint: c.Endpoint,
AccessToken: c.AccessToken,
AdditionalHeaders: c.AdditionalHeaders,
Flags: flags,
Out: out,
ProxyURL: c.ProxyURL,
ProxyPath: c.ProxyPath,
})
}

// Only use OAuth if we do not have SRC_ACCESS_TOKEN set
if c.AccessToken == "" {
if t, err := oauth.LoadToken(context.Background(), c.Endpoint); err == nil {
opts.OAuthToken = t
}
}

return api.NewClient(opts)
}

// readConfig reads the config file from the given path.
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/sourcegraph/sourcegraph/lib v0.0.0-20240709083501-1af563b61442
github.com/stretchr/testify v1.11.1
github.com/tliron/glsp v0.2.2
github.com/zalando/go-keyring v0.2.6
golang.org/x/sync v0.18.0
google.golang.org/api v0.256.0
google.golang.org/protobuf v1.36.10
Expand All @@ -41,6 +42,7 @@ require (
)

require (
al.essio.dev/pkg/shellescape v1.5.1 // indirect
cel.dev/expr v0.24.0 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
Expand All @@ -64,6 +66,7 @@ require (
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v24.0.4+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
Expand All @@ -78,6 +81,7 @@ require (
github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gofrs/uuid/v5 v5.0.0 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-containerregistry v0.19.1 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA=
Expand Down Expand Up @@ -139,6 +141,8 @@ github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglD
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down Expand Up @@ -212,6 +216,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
Expand Down Expand Up @@ -243,6 +249,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=
Expand Down Expand Up @@ -495,6 +503,8 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
Expand Down
52 changes: 39 additions & 13 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/kballard/go-shellquote"
"github.com/mattn/go-isatty"

"github.com/sourcegraph/src-cli/internal/oauth"
"github.com/sourcegraph/src-cli/internal/version"
)

Expand Down Expand Up @@ -85,21 +86,35 @@ type ClientOpts struct {

ProxyURL *url.URL
ProxyPath string

OAuthToken *oauth.Token
}

func buildTransport(opts ClientOpts, flags *Flags) *http.Transport {
transport := http.DefaultTransport.(*http.Transport).Clone()
func buildTransport(opts ClientOpts, flags *Flags) http.RoundTripper {
var transport http.RoundTripper
{
tp := http.DefaultTransport.(*http.Transport).Clone()

if flags.insecureSkipVerify != nil && *flags.insecureSkipVerify {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
if flags.insecureSkipVerify != nil && *flags.insecureSkipVerify {
tp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}

if transport.TLSClientConfig == nil {
transport.TLSClientConfig = &tls.Config{}
if tp.TLSClientConfig == nil {
tp.TLSClientConfig = &tls.Config{}
}

if opts.ProxyURL != nil || opts.ProxyPath != "" {
tp = withProxyTransport(tp, opts.ProxyURL, opts.ProxyPath)
}

transport = tp
}

if opts.ProxyURL != nil || opts.ProxyPath != "" {
transport = withProxyTransport(transport, opts.ProxyURL, opts.ProxyPath)
if opts.AccessToken == "" && opts.OAuthToken != nil {
transport = &oauth.Transport{
Base: transport,
Token: opts.OAuthToken,
}
}

return transport
Expand Down Expand Up @@ -168,6 +183,7 @@ func (c *client) createHTTPRequest(ctx context.Context, method, p string, body i
} else {
req.Header.Set("User-Agent", "src-cli/"+version.BuildTag)
}

if c.opts.AccessToken != "" {
req.Header.Set("Authorization", "token "+c.opts.AccessToken)
}
Expand Down Expand Up @@ -249,10 +265,20 @@ func (r *request) do(ctx context.Context, result any) (bool, error) {
// confirm the status code. You can test this easily with e.g. an invalid
// endpoint like -endpoint=https://google.com
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == http.StatusUnauthorized && isatty.IsCygwinTerminal(os.Stdout.Fd()) {
fmt.Println("You may need to specify or update your access token to use this endpoint.")
fmt.Println("See https://github.com/sourcegraph/src-cli#readme")
fmt.Println("")
if resp.StatusCode == http.StatusUnauthorized {
if oauth.IsOAuthTransport(r.client.httpClient.Transport) {
fmt.Println("The OAuth token is invalid. Please check that the Sourcegraph CLI client is still authorized.")
fmt.Println("")
fmt.Printf("To re-authorize, run: src login --oauth %s\n", r.client.opts.Endpoint)
fmt.Println("")
fmt.Println("Learn more at https://github.com/sourcegraph/src-cli#readme")
fmt.Println("")
}
if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
fmt.Println("You may need to specify or update your access token to use this endpoint.")
fmt.Println("See https://github.com/sourcegraph/src-cli#readme")
fmt.Println("")
}
}
body, err := io.ReadAll(resp.Body)
if err != nil {
Expand Down
Loading
Loading