diff --git a/cli/config/credentials/oauth_store.go b/cli/config/credentials/oauth_store.go index 399220c63e50..dc14ad2a8256 100644 --- a/cli/config/credentials/oauth_store.go +++ b/cli/config/credentials/oauth_store.go @@ -29,9 +29,10 @@ func NewOAuthStore(backingStore Store, manager oauth.Manager) Store { const minimumTokenLifetime = 50 * time.Minute // Get retrieves the credentials from the backing store, refreshing the -// access token if the retrieved token is valid for less than 50 minutes. -// If there are no credentials in the backing store, the device code flow -// is initiated with the tenant in order to log the user in and get +// access token if the stored credentials are valid for less than minimumTokenLifetime. +// If the credentials being retrieved are not for the official registry, they are +// returned as is. If the credentials retrieved do not parse as a token, they are +// also returned as is. func (o *oauthStore) Get(serverAddress string) (types.AuthConfig, error) { if serverAddress != registry.IndexServer { return o.backingStore.Get(serverAddress) @@ -52,8 +53,9 @@ func (o *oauthStore) Get(serverAddress string) (types.AuthConfig, error) { return auth, nil } - // if the access token is valid for less than 50 minutes, refresh it + // if the access token is valid for less than minimumTokenLifetime, refresh it if tokenRes.RefreshToken != "" && tokenRes.Claims.Expiry.Time().Before(time.Now().Add(minimumTokenLifetime)) { + // todo(laurazard): should use a context with a timeout here? refreshRes, err := o.manager.RefreshToken(context.TODO(), tokenRes.RefreshToken) if err != nil { return types.AuthConfig{}, err @@ -74,8 +76,9 @@ func (o *oauthStore) Get(serverAddress string) (types.AuthConfig, error) { }, nil } -// GetAll returns a map containing solely the auth config for the official -// registry, parsed from the backing store and refreshed if necessary. +// GetAll returns a map of all credentials in the backing store. If the backing +// store contains credentials for the official registry, these are refreshed/processed +// according to the same rules as Get. func (o *oauthStore) GetAll() (map[string]types.AuthConfig, error) { allAuths, err := o.backingStore.GetAll() if err != nil { @@ -98,8 +101,14 @@ func (o *oauthStore) GetAll() (map[string]types.AuthConfig, error) { // tenant if running func (o *oauthStore) Erase(serverAddress string) error { if serverAddress == registry.IndexServer { - // todo(laurazard): should this log out from the tenant - _ = o.manager.Logout(context.TODO()) + auth, err := o.backingStore.Get(registry.IndexServer) + if err != nil { + return err + } + if tokenRes, err := o.parseToken(auth.Password); err == nil { + // todo(laurazard): should use a context with a timeout here? + _ = o.manager.Logout(context.TODO(), tokenRes.RefreshToken) + } } return o.backingStore.Erase(serverAddress) } diff --git a/cli/config/credentials/oauth_store_test.go b/cli/config/credentials/oauth_store_test.go index 19287f8edc9b..cc24fac5b30a 100644 --- a/cli/config/credentials/oauth_store_test.go +++ b/cli/config/credentials/oauth_store_test.go @@ -286,13 +286,14 @@ func TestErase(t *testing.T) { t.Run("official registry", func(t *testing.T) { f := newStore(map[string]types.AuthConfig{ registry.IndexServer: { - Email: "foo@example.com", + Email: "foo@example.com", + Password: validNotExpiredToken + "..refresh-token", }, }) - var logoutCalled bool + var revokedToken string manager := &testManager{ - logout: func() error { - logoutCalled = true + logout: func(token string) error { + revokedToken = token return nil }, } @@ -304,7 +305,7 @@ func TestErase(t *testing.T) { assert.NilError(t, err) assert.Check(t, is.Len(f.GetAuthConfigs(), 0)) - assert.Check(t, logoutCalled) + assert.Equal(t, revokedToken, "refresh-token") }) t.Run("different registry", func(t *testing.T) { @@ -388,7 +389,7 @@ func TestStore(t *testing.T) { type testManager struct { loginDevice func() (oauth.TokenResult, error) - logout func() error + logout func(token string) error refresh func(token string) (oauth.TokenResult, error) } @@ -396,8 +397,8 @@ func (m *testManager) LoginDevice(_ context.Context, _ io.Writer) (oauth.TokenRe return m.loginDevice() } -func (m *testManager) Logout(_ context.Context) error { - return m.logout() +func (m *testManager) Logout(_ context.Context, token string) error { + return m.logout(token) } func (m *testManager) RefreshToken(_ context.Context, token string) (oauth.TokenResult, error) { diff --git a/cli/internal/oauth/api/api.go b/cli/internal/oauth/api/api.go index 539b68c21c3c..b5e791d9c68c 100644 --- a/cli/internal/oauth/api/api.go +++ b/cli/internal/oauth/api/api.go @@ -5,19 +5,21 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" + "runtime" "strings" "time" - "github.com/docker/cli/cli/internal/oauth/util" + "github.com/docker/cli/cli/version" ) type OAuthAPI interface { GetDeviceCode(ctx context.Context, audience string) (State, error) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) Refresh(ctx context.Context, token string) (TokenResponse, error) - LogoutURL() string + RevokeToken(ctx context.Context, refreshToken string) error } // API represents API interactions with Auth0. @@ -28,8 +30,6 @@ type API struct { ClientID string // Scopes are the scopes that are requested during the device auth flow. Scopes []string - // Client is the client that is used for calls. - Client util.Client } // TokenResponse represents the response of the /oauth/token route. @@ -46,7 +46,9 @@ type TokenResponse struct { var ErrTimeout = errors.New("timed out waiting for device token") -// GetDeviceCode returns device code authorization information from Auth0. +// GetDeviceCode initiates the device-code auth flow with the tenant. +// The state returned contains the device code that the user must use to +// authenticate, as well as the URL to visit, etc. func (a API) GetDeviceCode(ctx context.Context, audience string) (state State, err error) { data := url.Values{ "client_id": {a.ClientID}, @@ -55,7 +57,7 @@ func (a API) GetDeviceCode(ctx context.Context, audience string) (state State, e } deviceCodeURL := a.BaseURL + "/oauth/device/code" - resp, err := a.Client.PostForm(ctx, deviceCodeURL, strings.NewReader(data.Encode())) + resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode())) if err != nil { return } @@ -77,8 +79,10 @@ func (a API) GetDeviceCode(ctx context.Context, audience string) (state State, e return } -// WaitForDeviceToken polls to get tokens based on the device code set up. This -// only works in a device auth flow. +// WaitForDeviceToken polls the tenant to get access/refresh tokens for the user. +// This should be called after GetDeviceCode, and will block until the user has +// authenticated or we have reached the time limit for authenticating (based on +// the response from GetDeviceCode). func (a API) WaitForDeviceToken(ctx context.Context, state State) (TokenResponse, error) { ticker := time.NewTicker(state.IntervalDuration()) timeout := time.After(time.Duration(state.ExpiresIn) * time.Second) @@ -119,7 +123,7 @@ func (a API) getDeviceToken(ctx context.Context, state State) (res TokenResponse } oauthTokenURL := a.BaseURL + "/oauth/token" - resp, err := a.Client.PostForm(ctx, oauthTokenURL, strings.NewReader(data.Encode())) + resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode())) if err != nil { return res, fmt.Errorf("failed to get code: %w", err) } @@ -130,7 +134,7 @@ func (a API) getDeviceToken(ctx context.Context, state State) (res TokenResponse return } -// Refresh returns new tokens based on the refresh token. +// Refresh fetches new tokens using the refresh token. func (a API) Refresh(ctx context.Context, token string) (res TokenResponse, err error) { data := url.Values{ "grant_type": {"refresh_token"}, @@ -139,18 +143,46 @@ func (a API) Refresh(ctx context.Context, token string) (res TokenResponse, err } refreshURL := a.BaseURL + "/oauth/token" - //nolint:gosec // Ignore G107: Potential HTTP request made with variable url - resp, err := http.PostForm(refreshURL, data) + resp, err := postForm(ctx, refreshURL, strings.NewReader(data.Encode())) if err != nil { return } + defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&res) - _ = resp.Body.Close() - return } -func (a API) LogoutURL() string { - return fmt.Sprintf("%s/v2/logout?client_id=%s", a.BaseURL, a.ClientID) +// RevokeToken revokes a refresh token with the tenant so that it can no longer +// be used to get new tokens. +func (a API) RevokeToken(ctx context.Context, refreshToken string) error { + data := url.Values{ + "client_id": {a.ClientID}, + "token": {refreshToken}, + } + + revokeURL := a.BaseURL + "/oauth/revoke" + resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.New("failed to revoke token") + } + return nil +} + +func postForm(ctx context.Context, reqURL string, data io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, data) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + cliVersion := strings.ReplaceAll(version.Version, ".", "_") + req.Header.Set("User-Agent", fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH)) + + return http.DefaultClient.Do(req) } diff --git a/cli/internal/oauth/api/api_test.go b/cli/internal/oauth/api/api_test.go index 29a62e8da30e..c744886bf36b 100644 --- a/cli/internal/oauth/api/api_test.go +++ b/cli/internal/oauth/api/api_test.go @@ -8,7 +8,6 @@ import ( "testing" "time" - "github.com/docker/cli/cli/internal/oauth/util" "gotest.tools/v3/assert" ) @@ -38,7 +37,6 @@ func TestGetDeviceCode(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } state, err := api.GetDeviceCode(context.Background(), "anAudience") @@ -66,7 +64,6 @@ func TestGetDeviceCode(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } _, err := api.GetDeviceCode(context.Background(), "bad_audience") @@ -83,7 +80,6 @@ func TestGetDeviceCode(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } _, err := api.GetDeviceCode(context.Background(), "anAudience") @@ -101,7 +97,6 @@ func TestGetDeviceCode(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } ctx, cancel := context.WithCancel(context.Background()) @@ -155,7 +150,6 @@ func TestWaitForDeviceToken(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } state := State{ DeviceCode: "aDeviceCode", @@ -189,7 +183,6 @@ func TestWaitForDeviceToken(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } state := State{ DeviceCode: "aDeviceCode", @@ -217,7 +210,6 @@ func TestWaitForDeviceToken(t *testing.T) { BaseURL: ts.URL, ClientID: "aClientID", Scopes: []string{"bork", "meow"}, - Client: util.Client{}, } state := State{ DeviceCode: "aDeviceCode", @@ -236,3 +228,117 @@ func TestWaitForDeviceToken(t *testing.T) { assert.ErrorContains(t, err, "context canceled") }) } + +func TestRefresh(t *testing.T) { + t.Run("success", func(t *testing.T) { + expectedToken := TokenResponse{ + AccessToken: "a-real-token", + IDToken: "", + RefreshToken: "the-refresh-token", + Scope: "", + ExpiresIn: 3600, + TokenType: "", + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/token", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("refresh_token"), "v1.a-refresh-token") + assert.Equal(t, r.FormValue("grant_type"), "refresh_token") + + jsonState, err := json.Marshal(expectedToken) + assert.NilError(t, err) + w.Write(jsonState) + })) + defer ts.Close() + api := API{ + BaseURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + token, err := api.Refresh(context.Background(), "v1.a-refresh-token") + assert.NilError(t, err) + + assert.DeepEqual(t, token, expectedToken) + }) + + t.Run("canceled context", func(t *testing.T) { + expectedToken := TokenResponse{ + AccessToken: "a-real-token", + IDToken: "", + RefreshToken: "the-refresh-token", + Scope: "", + ExpiresIn: 3600, + TokenType: "", + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/token", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("refresh_token"), "v1.a-refresh-token") + assert.Equal(t, r.FormValue("grant_type"), "refresh_token") + + jsonState, err := json.Marshal(expectedToken) + assert.NilError(t, err) + w.Write(jsonState) + })) + defer ts.Close() + api := API{ + BaseURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err := api.Refresh(ctx, "v1.a-refresh-token") + + assert.ErrorContains(t, err, "context canceled") + }) +} + +func TestRevoke(t *testing.T) { + t.Run("success", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/revoke", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + api := API{ + BaseURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + + err := api.RevokeToken(context.Background(), "v1.a-refresh-token") + assert.NilError(t, err) + }) + + t.Run("canceled context", func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + assert.Equal(t, "/oauth/revoke", r.URL.Path) + assert.Equal(t, r.FormValue("client_id"), "aClientID") + assert.Equal(t, r.FormValue("token"), "v1.a-refresh-token") + + w.WriteHeader(http.StatusOK) + })) + defer ts.Close() + api := API{ + BaseURL: ts.URL, + ClientID: "aClientID", + Scopes: []string{"bork", "meow"}, + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + err := api.RevokeToken(ctx, "v1.a-refresh-token") + + assert.ErrorContains(t, err, "context canceled") + }) +} diff --git a/cli/internal/oauth/manager/manager.go b/cli/internal/oauth/manager/manager.go index c5477d867114..5bbaee442f35 100644 --- a/cli/internal/oauth/manager/manager.go +++ b/cli/internal/oauth/manager/manager.go @@ -10,8 +10,9 @@ import ( "strings" "github.com/docker/cli/cli/internal/oauth/api" - "github.com/docker/cli/cli/internal/oauth/util" "github.com/docker/cli/cli/oauth" + + "github.com/pkg/browser" ) // OAuthManager is the manager @@ -22,7 +23,7 @@ type OAuthManager struct { openBrowser func(string) error } -// OAuthManagerOptions is the options used for New to create a new auth manager. +// OAuthManagerOptions are the options used for New to create a new auth manager. type OAuthManagerOptions struct { Audience string ClientID string @@ -38,7 +39,7 @@ func New(options OAuthManagerOptions) *OAuthManager { scopes = options.Scopes } - openBrowser := util.OpenBrowser + openBrowser := browser.OpenURL if options.OpenBrowser != nil { openBrowser = options.OpenBrowser } @@ -49,18 +50,16 @@ func New(options OAuthManagerOptions) *OAuthManager { BaseURL: "https://" + options.Tenant, ClientID: options.ClientID, Scopes: scopes, - Client: util.Client{ - UserAgent: options.DeviceName, - }, }, tenant: options.Tenant, openBrowser: openBrowser, } } -// LoginDevice launches the device authentication flow with the tenant, printing instructions -// to the provided writer and attempting to open the browser for the user to authenticate. -// Once complete, the retrieved tokens are stored and returned. +// LoginDevice launches the device authentication flow with the tenant, printing +// instructions to the provided writer and attempting to open the browser for +// the user to authenticate. +// Once complete, the retrieved tokens are returned. func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (res oauth.TokenResult, err error) { state, err := m.api.GetDeviceCode(ctx, m.audience) if err != nil { @@ -75,7 +74,7 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (res oauth. _, _ = fmt.Fprintln(w, "To sign in with credentials on the command line, use 'docker login -u '") _, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: %s\n", state.UserCode) _, _ = fmt.Fprint(w, "\nPress ENTER to open the browser.\n") - _, _ = fmt.Fprintf(w, "Or open the URL manually: %s.\n", strings.Split(state.VerificationURI, "?")[0]) + _, _ = fmt.Fprintf(w, "Or open the URL manually: %s\n", strings.Split(state.VerificationURI, "?")[0]) tokenResChan := make(chan api.TokenResponse) waitForTokenErrChan := make(chan error) @@ -117,25 +116,14 @@ func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (res oauth. return res, nil } -// Logout logs out of the session for the client and removes tokens from the storage provider. -func (m *OAuthManager) Logout(ctx context.Context) error { - return errors.Join( - m.openBrowser(m.api.LogoutURL()), - ) +// Logout revokes the provided refresh token with the oauth tenant. However, it +// does not end the user's session with the tenant. +func (m *OAuthManager) Logout(ctx context.Context, refreshToken string) error { + return m.api.RevokeToken(ctx, refreshToken) } -var ( - // ErrNoCreds is returned by RefreshToken when the store does not contain credentials - // for the official registry. - ErrNoCreds = errors.New("no credentials found") - - // ErrUnexpiredToken is returned by RefreshToken when the token is not expired. - ErrUnexpiredToken = errors.New("token is not expired") -) - -// RefreshToken fetches credentials from the store, refreshes them, stores the new tokens -// and returns them. -// If there are no credentials in the store, ErrNoCreds is returned. +// RefreshToken uses the provided token to refresh access with the oauth +// tenant, returning new access and refresh token. func (m OAuthManager) RefreshToken(ctx context.Context, refreshToken string) (res oauth.TokenResult, err error) { refreshRes, err := m.api.Refresh(ctx, refreshToken) if err != nil { diff --git a/cli/internal/oauth/manager/manager_test.go b/cli/internal/oauth/manager/manager_test.go index 9945605f04cb..6ce81de71648 100644 --- a/cli/internal/oauth/manager/manager_test.go +++ b/cli/internal/oauth/manager/manager_test.go @@ -176,22 +176,21 @@ func TestLoginDevice(t *testing.T) { } func TestLogout(t *testing.T) { + var receivedToken string a := &testAPI{ - logoutURL: "test-logout-url", + revokeToken: func(token string) error { + receivedToken = token + return nil + }, } - var browserOpenURL string manager := OAuthManager{ api: a, - openBrowser: func(url string) error { - browserOpenURL = url - return nil - }, } - err := manager.Logout(context.Background()) + err := manager.Logout(context.Background(), "a-refresh-token") assert.NilError(t, err) - assert.Equal(t, browserOpenURL, "test-logout-url") + assert.Equal(t, receivedToken, "a-refresh-token") } func TestRefreshToken(t *testing.T) { @@ -240,10 +239,10 @@ func TestRefreshToken(t *testing.T) { var _ api.OAuthAPI = &testAPI{} type testAPI struct { - logoutURL string getDeviceToken func(audience string) (api.State, error) waitForDeviceToken func(state api.State) (api.TokenResponse, error) refresh func(token string) (api.TokenResponse, error) + revokeToken func(token string) error } func (t *testAPI) GetDeviceCode(_ context.Context, audience string) (api.State, error) { @@ -267,6 +266,9 @@ func (t *testAPI) Refresh(_ context.Context, token string) (api.TokenResponse, e return api.TokenResponse{}, nil } -func (t *testAPI) LogoutURL() string { - return t.logoutURL +func (t *testAPI) RevokeToken(_ context.Context, token string) error { + if t.revokeToken != nil { + return t.revokeToken(token) + } + return nil } diff --git a/cli/internal/oauth/util/client.go b/cli/internal/oauth/util/client.go deleted file mode 100644 index d11fa7fd1f52..000000000000 --- a/cli/internal/oauth/util/client.go +++ /dev/null @@ -1,52 +0,0 @@ -package util - -import ( - "context" - "io" - "net/http" -) - -// Client is a client and actions for interacting with the tenant auth API. -type Client struct { - UserAgent string -} - -// setHeaders sets common headers for requests. -func (c Client) setHeaders(req *http.Request, isForm bool) { - if isForm { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - - if c.UserAgent != "" { - req.Header.Set("User-Agent", c.UserAgent) - } -} - -// PostForm does a POST request with form data. -func (c Client) PostForm(ctx context.Context, url string, data io.Reader) (*http.Response, error) { - client := http.Client{} - - req, err := http.NewRequest(http.MethodPost, url, data) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - - c.setHeaders(req, true) - - return client.Do(req) -} - -// Post does a POST with the specified data. -func (c Client) Post(url string, data io.Reader) (*http.Response, error) { - client := http.Client{} - - req, err := http.NewRequest(http.MethodPost, url, data) - if err != nil { - return nil, err - } - - c.setHeaders(req, false) - - return client.Do(req) -} diff --git a/cli/internal/oauth/util/util.go b/cli/internal/oauth/util/util.go deleted file mode 100644 index ac18953af008..000000000000 --- a/cli/internal/oauth/util/util.go +++ /dev/null @@ -1,23 +0,0 @@ -package util - -import ( - "errors" - "os/exec" - "runtime" -) - -// OpenBrowser opens the specified URL in a browser based on OS. -func OpenBrowser(url string) (err error) { - switch runtime.GOOS { - case "linux": - err = exec.Command("xdg-open", url).Start() - case "windows": - err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() - case "darwin": - err = exec.Command("open", url).Start() - default: - err = errors.New("unsupported platform") - } - - return -} diff --git a/cli/oauth/manager.go b/cli/oauth/manager.go index e0e3be283aba..56389e918a4a 100644 --- a/cli/oauth/manager.go +++ b/cli/oauth/manager.go @@ -16,6 +16,6 @@ type TokenResult struct { type Manager interface { LoginDevice(ctx context.Context, out io.Writer) (TokenResult, error) - Logout(ctx context.Context) error + Logout(ctx context.Context, refreshToken string) error RefreshToken(ctx context.Context, refreshToken string) (TokenResult, error) } diff --git a/internal/test/cli.go b/internal/test/cli.go index ebd3ffbb16df..080e4037952b 100644 --- a/internal/test/cli.go +++ b/internal/test/cli.go @@ -224,7 +224,7 @@ func (f *fakeOauthManager) LoginDevice(ctx context.Context, w io.Writer) (res oa return res, nil } -func (f *fakeOauthManager) Logout(ctx context.Context) error { +func (f *fakeOauthManager) Logout(ctx context.Context, refreshToken string) error { return nil } diff --git a/vendor.mod b/vendor.mod index 2fbc5110ae36..1601c9fc310d 100644 --- a/vendor.mod +++ b/vendor.mod @@ -79,6 +79,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/symlink v0.2.0 // indirect github.com/moby/sys/user v0.1.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/prometheus/client_golang v1.17.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.44.0 // indirect diff --git a/vendor.sum b/vendor.sum index 8a88cbbc660e..f769fc02e5de 100644 --- a/vendor.sum +++ b/vendor.sum @@ -213,6 +213,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -378,6 +380,7 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/vendor/github.com/pkg/browser/LICENSE b/vendor/github.com/pkg/browser/LICENSE new file mode 100644 index 000000000000..65f78fb62910 --- /dev/null +++ b/vendor/github.com/pkg/browser/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/browser/README.md b/vendor/github.com/pkg/browser/README.md new file mode 100644 index 000000000000..72b1976e3035 --- /dev/null +++ b/vendor/github.com/pkg/browser/README.md @@ -0,0 +1,55 @@ + +# browser + import "github.com/pkg/browser" + +Package browser provides helpers to open files, readers, and urls in a browser window. + +The choice of which browser is started is entirely client dependant. + + + + + +## Variables +``` go +var Stderr io.Writer = os.Stderr +``` +Stderr is the io.Writer to which executed commands write standard error. + +``` go +var Stdout io.Writer = os.Stdout +``` +Stdout is the io.Writer to which executed commands write standard output. + + +## func OpenFile +``` go +func OpenFile(path string) error +``` +OpenFile opens new browser window for the file path. + + +## func OpenReader +``` go +func OpenReader(r io.Reader) error +``` +OpenReader consumes the contents of r and presents the +results in a new browser window. + + +## func OpenURL +``` go +func OpenURL(url string) error +``` +OpenURL opens a new browser window pointing to url. + + + + + + + + + +- - - +Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md) diff --git a/vendor/github.com/pkg/browser/browser.go b/vendor/github.com/pkg/browser/browser.go new file mode 100644 index 000000000000..d7969d74d80d --- /dev/null +++ b/vendor/github.com/pkg/browser/browser.go @@ -0,0 +1,57 @@ +// Package browser provides helpers to open files, readers, and urls in a browser window. +// +// The choice of which browser is started is entirely client dependant. +package browser + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" +) + +// Stdout is the io.Writer to which executed commands write standard output. +var Stdout io.Writer = os.Stdout + +// Stderr is the io.Writer to which executed commands write standard error. +var Stderr io.Writer = os.Stderr + +// OpenFile opens new browser window for the file path. +func OpenFile(path string) error { + path, err := filepath.Abs(path) + if err != nil { + return err + } + return OpenURL("file://" + path) +} + +// OpenReader consumes the contents of r and presents the +// results in a new browser window. +func OpenReader(r io.Reader) error { + f, err := ioutil.TempFile("", "browser.*.html") + if err != nil { + return fmt.Errorf("browser: could not create temporary file: %v", err) + } + if _, err := io.Copy(f, r); err != nil { + f.Close() + return fmt.Errorf("browser: caching temporary file failed: %v", err) + } + if err := f.Close(); err != nil { + return fmt.Errorf("browser: caching temporary file failed: %v", err) + } + return OpenFile(f.Name()) +} + +// OpenURL opens a new browser window pointing to url. +func OpenURL(url string) error { + return openBrowser(url) +} + +func runCmd(prog string, args ...string) error { + cmd := exec.Command(prog, args...) + cmd.Stdout = Stdout + cmd.Stderr = Stderr + return cmd.Run() +} diff --git a/vendor/github.com/pkg/browser/browser_darwin.go b/vendor/github.com/pkg/browser/browser_darwin.go new file mode 100644 index 000000000000..8507cf7c2b45 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_darwin.go @@ -0,0 +1,5 @@ +package browser + +func openBrowser(url string) error { + return runCmd("open", url) +} diff --git a/vendor/github.com/pkg/browser/browser_freebsd.go b/vendor/github.com/pkg/browser/browser_freebsd.go new file mode 100644 index 000000000000..4fc7ff0761b4 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_freebsd.go @@ -0,0 +1,14 @@ +package browser + +import ( + "errors" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { + return errors.New("xdg-open: command not found - install xdg-utils from ports(8)") + } + return err +} diff --git a/vendor/github.com/pkg/browser/browser_linux.go b/vendor/github.com/pkg/browser/browser_linux.go new file mode 100644 index 000000000000..d26cdddf9c15 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_linux.go @@ -0,0 +1,21 @@ +package browser + +import ( + "os/exec" + "strings" +) + +func openBrowser(url string) error { + providers := []string{"xdg-open", "x-www-browser", "www-browser"} + + // There are multiple possible providers to open a browser on linux + // One of them is xdg-open, another is x-www-browser, then there's www-browser, etc. + // Look for one that exists and run it + for _, provider := range providers { + if _, err := exec.LookPath(provider); err == nil { + return runCmd(provider, url) + } + } + + return &exec.Error{Name: strings.Join(providers, ","), Err: exec.ErrNotFound} +} diff --git a/vendor/github.com/pkg/browser/browser_netbsd.go b/vendor/github.com/pkg/browser/browser_netbsd.go new file mode 100644 index 000000000000..65a5e5a29342 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_netbsd.go @@ -0,0 +1,14 @@ +package browser + +import ( + "errors" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { + return errors.New("xdg-open: command not found - install xdg-utils from pkgsrc(7)") + } + return err +} diff --git a/vendor/github.com/pkg/browser/browser_openbsd.go b/vendor/github.com/pkg/browser/browser_openbsd.go new file mode 100644 index 000000000000..4fc7ff0761b4 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_openbsd.go @@ -0,0 +1,14 @@ +package browser + +import ( + "errors" + "os/exec" +) + +func openBrowser(url string) error { + err := runCmd("xdg-open", url) + if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { + return errors.New("xdg-open: command not found - install xdg-utils from ports(8)") + } + return err +} diff --git a/vendor/github.com/pkg/browser/browser_unsupported.go b/vendor/github.com/pkg/browser/browser_unsupported.go new file mode 100644 index 000000000000..7c5c17d34d26 --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_unsupported.go @@ -0,0 +1,12 @@ +// +build !linux,!windows,!darwin,!openbsd,!freebsd,!netbsd + +package browser + +import ( + "fmt" + "runtime" +) + +func openBrowser(url string) error { + return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS) +} diff --git a/vendor/github.com/pkg/browser/browser_windows.go b/vendor/github.com/pkg/browser/browser_windows.go new file mode 100644 index 000000000000..63e192959a5e --- /dev/null +++ b/vendor/github.com/pkg/browser/browser_windows.go @@ -0,0 +1,7 @@ +package browser + +import "golang.org/x/sys/windows" + +func openBrowser(url string) error { + return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b395aa3f3ccd..1540120b0e39 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -237,6 +237,9 @@ github.com/opencontainers/go-digest ## explicit; go 1.18 github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go/v1 +# github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c +## explicit; go 1.14 +github.com/pkg/browser # github.com/pkg/errors v0.9.1 ## explicit github.com/pkg/errors