Merge pull request #6054 from thaJeztah/unify_internal
move cli/internal/ packages to top-level internal/
This commit is contained in:
@@ -19,9 +19,9 @@ import (
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
|
||||
@@ -19,9 +19,9 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/image/build"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
dockeropts "github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/api/types/auxprogress"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
)
|
||||
|
||||
// WaitOnService waits for the service to converge. It outputs a progress bar,
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/idresolver"
|
||||
"github.com/docker/cli/cli/internal/logdetails"
|
||||
"github.com/docker/cli/internal/logdetails"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/swarm/progress"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package jsonstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
)
|
||||
|
||||
type (
|
||||
Stream = jsonmessage.Stream
|
||||
JSONMessage = jsonmessage.JSONMessage
|
||||
JSONError = jsonmessage.JSONError
|
||||
JSONProgress = jsonmessage.JSONProgress
|
||||
)
|
||||
|
||||
type ctxReader struct {
|
||||
err chan error
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (r *ctxReader) Read(p []byte) (n int, err error) {
|
||||
select {
|
||||
case err = <-r.err:
|
||||
return 0, err
|
||||
default:
|
||||
return r.r.Read(p)
|
||||
}
|
||||
}
|
||||
|
||||
type Options func(*options)
|
||||
|
||||
type options struct {
|
||||
AuxCallback func(JSONMessage)
|
||||
}
|
||||
|
||||
func WithAuxCallback(cb func(JSONMessage)) Options {
|
||||
return func(o *options) {
|
||||
o.AuxCallback = cb
|
||||
}
|
||||
}
|
||||
|
||||
// Display prints the JSON messages from the given reader to the given stream.
|
||||
//
|
||||
// It wraps the [jsonmessage.DisplayJSONMessagesStream] function to make it
|
||||
// "context aware" and appropriately returns why the function was canceled.
|
||||
//
|
||||
// It returns an error if the context is canceled, but not if the input reader / stream is closed.
|
||||
func Display(ctx context.Context, in io.Reader, stream Stream, opts ...Options) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
reader := &ctxReader{err: make(chan error, 1), r: in}
|
||||
stopFunc := context.AfterFunc(ctx, func() { reader.err <- ctx.Err() })
|
||||
defer stopFunc()
|
||||
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
if err := jsonmessage.DisplayJSONMessagesStream(reader, stream, stream.FD(), stream.IsTerminal(), o.AuxCallback); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package jsonstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestDisplay(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
client, server := io.Pipe()
|
||||
t.Cleanup(func() {
|
||||
assert.NilError(t, server.Close())
|
||||
})
|
||||
|
||||
go func() {
|
||||
enc := json.NewEncoder(server)
|
||||
for i := 0; i < 100; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
assert.NilError(t, server.Close(), "failed to close jsonmessage server")
|
||||
return
|
||||
default:
|
||||
err := enc.Encode(JSONMessage{
|
||||
Status: "Downloading",
|
||||
ID: fmt.Sprintf("id-%d", i),
|
||||
TimeNano: time.Now().UnixNano(),
|
||||
Time: time.Now().Unix(),
|
||||
Progress: &JSONProgress{
|
||||
Current: int64(i),
|
||||
Total: 100,
|
||||
Start: 0,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
streamCtx, cancelStream := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancelStream)
|
||||
|
||||
done := make(chan error)
|
||||
go func() {
|
||||
out := streams.NewOut(io.Discard)
|
||||
done <- Display(streamCtx, client, out)
|
||||
}()
|
||||
|
||||
cancelStream()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Second * 3):
|
||||
case err := <-done:
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Package logdetails contains tools for parsing docker log lines.
|
||||
package logdetails
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parse parses a string of key value pairs in the form
|
||||
// "k=v,l=w", where the keys and values are url query escaped, and each pair
|
||||
// is separated by a comma. Returns a map of the key value pairs on success,
|
||||
// and an error if the details string is not in a valid format.
|
||||
//
|
||||
// The details string encoding is implemented in
|
||||
// github.com/moby/moby/api/server/httputils/write_log_stream.go
|
||||
func Parse(details string) (map[string]string, error) {
|
||||
pairs := strings.Split(details, ",")
|
||||
detailsMap := make(map[string]string, len(pairs))
|
||||
for _, pair := range pairs {
|
||||
k, v, ok := strings.Cut(pair, "=")
|
||||
if !ok || k == "" {
|
||||
// missing equal sign, or no key.
|
||||
return nil, errors.New("invalid details format")
|
||||
}
|
||||
var err error
|
||||
k, err = url.QueryUnescape(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err = url.QueryUnescape(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detailsMap[k] = v
|
||||
}
|
||||
return detailsMap, nil
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package logdetails
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
expected map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
line: "key=value",
|
||||
expected: map[string]string{"key": "value"},
|
||||
},
|
||||
{
|
||||
line: "key1=value1,key2=value2",
|
||||
expected: map[string]string{"key1": "value1", "key2": "value2"},
|
||||
},
|
||||
{
|
||||
line: "key+with+spaces=value%3Dequals,asdf%2C=",
|
||||
expected: map[string]string{"key with spaces": "value=equals", "asdf,": ""},
|
||||
},
|
||||
{
|
||||
line: "key=,key2=",
|
||||
expected: map[string]string{"key": "", "key2": ""},
|
||||
},
|
||||
{
|
||||
line: "key=,=nothing",
|
||||
expectedErr: "invalid details format",
|
||||
},
|
||||
{
|
||||
line: "=nothing",
|
||||
expectedErr: "invalid details format",
|
||||
},
|
||||
{
|
||||
line: "=",
|
||||
expectedErr: "invalid details format",
|
||||
},
|
||||
{
|
||||
line: "errors",
|
||||
expectedErr: "invalid details format",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.line, func(t *testing.T) {
|
||||
actual, err := Parse(tc.line)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.Error(err, tc.expectedErr))
|
||||
} else {
|
||||
assert.Check(t, err)
|
||||
}
|
||||
assert.Check(t, is.DeepEqual(tc.expected, actual))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.23
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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)
|
||||
RevokeToken(ctx context.Context, refreshToken string) error
|
||||
GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error)
|
||||
}
|
||||
|
||||
// API represents API interactions with Auth0.
|
||||
type API struct {
|
||||
// TenantURL is the base used for each request to Auth0.
|
||||
TenantURL string
|
||||
// ClientID is the client ID for the application to auth with the tenant.
|
||||
ClientID string
|
||||
// Scopes are the scopes that are requested during the device auth flow.
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
// TokenResponse represents the response of the /oauth/token route.
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
Scope string `json:"scope"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Error *string `json:"error,omitempty"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
}
|
||||
|
||||
var ErrTimeout = errors.New("timed out waiting for device token")
|
||||
|
||||
// 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, error) {
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"audience": {audience},
|
||||
"scope": {strings.Join(a.Scopes, " ")},
|
||||
}
|
||||
|
||||
deviceCodeURL := a.TenantURL + "/oauth/device/code"
|
||||
resp, err := postForm(ctx, deviceCodeURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return State{}, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return State{}, tryDecodeOAuthError(resp)
|
||||
}
|
||||
|
||||
var state State
|
||||
err = json.NewDecoder(resp.Body).Decode(&state)
|
||||
if err != nil {
|
||||
return state, fmt.Errorf("failed to get device code: %w", err)
|
||||
}
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
func tryDecodeOAuthError(resp *http.Response) error {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err == nil {
|
||||
if errorDescription, ok := body["error_description"].(string); ok {
|
||||
return errors.New(errorDescription)
|
||||
}
|
||||
}
|
||||
return errors.New("unexpected response from tenant: " + resp.Status)
|
||||
}
|
||||
|
||||
// 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 for polling tenant for login – based on the interval
|
||||
// specified by the tenant response.
|
||||
ticker := time.NewTimer(state.IntervalDuration())
|
||||
defer ticker.Stop()
|
||||
// The tenant tells us for as long as we can poll it for credentials
|
||||
// while the user logs in through their browser. Timeout if we don't get
|
||||
// credentials within this period.
|
||||
timeout := time.NewTimer(state.ExpiryDuration())
|
||||
defer timeout.Stop()
|
||||
|
||||
for {
|
||||
resetTimer(ticker, state.IntervalDuration())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// user canceled login
|
||||
return TokenResponse{}, ctx.Err()
|
||||
case <-ticker.C:
|
||||
// tick, check for user login
|
||||
res, err := a.getDeviceToken(ctx, state)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
// if the caller canceled the context, continue
|
||||
// and let the select hit the ctx.Done() branch
|
||||
continue
|
||||
}
|
||||
return TokenResponse{}, err
|
||||
}
|
||||
|
||||
if res.Error != nil {
|
||||
if *res.Error == "authorization_pending" {
|
||||
continue
|
||||
}
|
||||
|
||||
return res, errors.New(res.ErrorDescription)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
case <-timeout.C:
|
||||
// login timed out
|
||||
return TokenResponse{}, ErrTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimer is a helper function thatstops, drains and resets the timer.
|
||||
// This is necessary in go versions <1.23, since the timer isn't stopped +
|
||||
// the timer's channel isn't drained on timer.Reset.
|
||||
// See: https://go-review.googlesource.com/c/go/+/568341
|
||||
// FIXME: remove/simplify this after we update to go1.23
|
||||
func resetTimer(t *time.Timer, d time.Duration) {
|
||||
if !t.Stop() {
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
t.Reset(d)
|
||||
}
|
||||
|
||||
// getDeviceToken calls the token endpoint of Auth0 and returns the response.
|
||||
func (a API) getDeviceToken(ctx context.Context, state State) (TokenResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
data := url.Values{
|
||||
"client_id": {a.ClientID},
|
||||
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
|
||||
"device_code": {state.DeviceCode},
|
||||
}
|
||||
oauthTokenURL := a.TenantURL + "/oauth/token"
|
||||
|
||||
resp, err := postForm(ctx, oauthTokenURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return TokenResponse{}, fmt.Errorf("failed to get tokens: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
// this endpoint returns a 403 with an `authorization_pending` error until the
|
||||
// user has authenticated, so we don't check the status code here and instead
|
||||
// decode the response and check for the error.
|
||||
var res TokenResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// 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.TenantURL + "/oauth/revoke"
|
||||
resp, err := postForm(ctx, revokeURL, strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return tryDecodeOAuthError(resp)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (API) GetAutoPAT(ctx context.Context, audience string, res TokenResponse) (string, error) {
|
||||
patURL := audience + "/v2/access-tokens/desktop-generate"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, patURL, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+res.AccessToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return "", fmt.Errorf("unexpected response from Hub: %s", resp.Status)
|
||||
}
|
||||
|
||||
var response patGenerateResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return response.Data.Token, nil
|
||||
}
|
||||
|
||||
type patGenerateResponse struct {
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
}
|
||||
@@ -1,428 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestGetDeviceCode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var clientID, audience, scope, path string
|
||||
expectedState := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
VerificationURI: "aVerificationURI",
|
||||
ExpiresIn: 60,
|
||||
}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
clientID = r.FormValue("client_id")
|
||||
audience = r.FormValue("audience")
|
||||
scope = r.FormValue("scope")
|
||||
path = r.URL.Path
|
||||
|
||||
jsonState, err := json.Marshal(expectedState)
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, _ = w.Write(jsonState)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
state, err := api.GetDeviceCode(context.Background(), "anAudience")
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, expectedState, state)
|
||||
assert.Equal(t, clientID, "aClientID")
|
||||
assert.Equal(t, audience, "anAudience")
|
||||
assert.Equal(t, scope, "bork meow")
|
||||
assert.Equal(t, path, "/oauth/device/code")
|
||||
})
|
||||
|
||||
t.Run("error w/ description", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
jsonState, err := json.Marshal(TokenResponse{
|
||||
ErrorDescription: "invalid audience",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write(jsonState)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
_, err := api.GetDeviceCode(context.Background(), "bad_audience")
|
||||
|
||||
assert.ErrorContains(t, err, "invalid audience")
|
||||
})
|
||||
|
||||
t.Run("general error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Error(w, "an error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
_, err := api.GetDeviceCode(context.Background(), "anAudience")
|
||||
|
||||
assert.ErrorContains(t, err, "unexpected response from tenant: 500 Internal Server Error")
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
http.Error(w, "an error", http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cancel()
|
||||
}()
|
||||
_, err := api.GetDeviceCode(ctx, "anAudience")
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWaitForDeviceToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
expectedToken := TokenResponse{
|
||||
AccessToken: "a-real-token",
|
||||
IDToken: "",
|
||||
RefreshToken: "the-refresh-token",
|
||||
Scope: "",
|
||||
ExpiresIn: 3600,
|
||||
TokenType: "",
|
||||
}
|
||||
var respond atomic.Bool
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
respond.Store(true)
|
||||
}()
|
||||
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("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
|
||||
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
|
||||
|
||||
if respond.Load() {
|
||||
jsonState, err := json.Marshal(expectedToken)
|
||||
assert.NilError(t, err)
|
||||
w.Write(jsonState)
|
||||
} else {
|
||||
pendingError := "authorization_pending"
|
||||
jsonResponse, err := json.Marshal(TokenResponse{
|
||||
Error: &pendingError,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.Write(jsonResponse)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
state := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
Interval: 1,
|
||||
ExpiresIn: 30,
|
||||
}
|
||||
token, err := api.WaitForDeviceToken(context.Background(), state)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, token, expectedToken)
|
||||
})
|
||||
|
||||
t.Run("timeout", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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("grant_type"), "urn:ietf:params:oauth:grant-type:device_code")
|
||||
assert.Equal(t, r.FormValue("device_code"), "aDeviceCode")
|
||||
|
||||
pendingError := "authorization_pending"
|
||||
jsonResponse, err := json.Marshal(TokenResponse{
|
||||
Error: &pendingError,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.Write(jsonResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
state := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
Interval: 5,
|
||||
ExpiresIn: 1,
|
||||
}
|
||||
|
||||
_, err := api.WaitForDeviceToken(context.Background(), state)
|
||||
|
||||
assert.ErrorIs(t, err, ErrTimeout)
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
pendingError := "authorization_pending"
|
||||
jsonResponse, err := json.Marshal(TokenResponse{
|
||||
Error: &pendingError,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.Write(jsonResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
state := State{
|
||||
DeviceCode: "aDeviceCode",
|
||||
UserCode: "aUserCode",
|
||||
Interval: 1,
|
||||
ExpiresIn: 5,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(1 * time.Second)
|
||||
cancel()
|
||||
}()
|
||||
_, err := api.WaitForDeviceToken(ctx, state)
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.NilError(t, err)
|
||||
})
|
||||
|
||||
t.Run("unexpected response", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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.StatusNotFound)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.ErrorContains(t, err, "unexpected response from tenant: 404 Not Found")
|
||||
})
|
||||
|
||||
t.Run("error w/ description", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
jsonState, err := json.Marshal(TokenResponse{
|
||||
ErrorDescription: "invalid client id",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write(jsonState)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
err := api.RevokeToken(context.Background(), "v1.a-refresh-token")
|
||||
assert.ErrorContains(t, err, "invalid client id")
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
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{
|
||||
TenantURL: 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")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAutoPAT(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("success", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
|
||||
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
marshalledResponse, err := json.Marshal(patGenerateResponse{
|
||||
Data: struct {
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
Token: "a-docker-pat",
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(marshalledResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
pat, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, "a-docker-pat", pat)
|
||||
})
|
||||
|
||||
t.Run("general error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
|
||||
_, err := api.GetAutoPAT(context.Background(), ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
assert.ErrorContains(t, err, "unexpected response from Hub: 500 Internal Server Error")
|
||||
})
|
||||
|
||||
t.Run("context canceled", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "POST", r.Method)
|
||||
assert.Equal(t, "/v2/access-tokens/desktop-generate", r.URL.Path)
|
||||
assert.Equal(t, "Bearer bork", r.Header.Get("Authorization"))
|
||||
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||
|
||||
marshalledResponse, err := json.Marshal(patGenerateResponse{
|
||||
Data: struct {
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
Token: "a-docker-pat",
|
||||
},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write(marshalledResponse)
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := API{
|
||||
TenantURL: ts.URL,
|
||||
ClientID: "aClientID",
|
||||
Scopes: []string{"bork", "meow"},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
pat, err := api.GetAutoPAT(ctx, ts.URL, TokenResponse{
|
||||
AccessToken: "bork",
|
||||
})
|
||||
|
||||
assert.ErrorContains(t, err, "context canceled")
|
||||
assert.Equal(t, "", pat)
|
||||
})
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// State represents the state of exchange after submitting.
|
||||
type State struct {
|
||||
DeviceCode string `json:"device_code"`
|
||||
UserCode string `json:"user_code"`
|
||||
VerificationURI string `json:"verification_uri_complete"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
|
||||
// IntervalDuration returns the duration that should be waited between each auth
|
||||
// polling event.
|
||||
func (s State) IntervalDuration() time.Duration {
|
||||
return time.Second * time.Duration(s.Interval)
|
||||
}
|
||||
|
||||
// ExpiryDuration returns the total duration for which the client should keep
|
||||
// polling.
|
||||
func (s State) ExpiryDuration() time.Duration {
|
||||
return time.Second * time.Duration(s.ExpiresIn)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
)
|
||||
|
||||
// Claims represents standard claims along with some custom ones.
|
||||
type Claims struct {
|
||||
jwt.Claims
|
||||
|
||||
// Domain is the domain claims for the token.
|
||||
Domain DomainClaims `json:"https://hub.docker.com"`
|
||||
|
||||
// Scope is the scopes for the claims as a string that is space delimited.
|
||||
Scope string `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// DomainClaims represents a custom claim data set that doesn't change the spec
|
||||
// payload. This is primarily introduced by Auth0 and is defined by a fully
|
||||
// specified URL as it's key. e.g. "https://hub.docker.com"
|
||||
type DomainClaims struct {
|
||||
// UUID is the user, machine client, or organization's UUID in our database.
|
||||
UUID string `json:"uuid"`
|
||||
|
||||
// Email is the user's email address.
|
||||
Email string `json:"email"`
|
||||
|
||||
// Username is the user's username.
|
||||
Username string `json:"username"`
|
||||
|
||||
// Source is the source of the JWT. This should look like
|
||||
// `docker_{type}|{id}`.
|
||||
Source string `json:"source"`
|
||||
|
||||
// SessionID is the unique ID of the token.
|
||||
SessionID string `json:"session_id"`
|
||||
|
||||
// ClientID is the client_id that generated the token. This is filled if
|
||||
// M2M.
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
|
||||
// ClientName is the name of the client that generated the token. This is
|
||||
// filled if M2M.
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
}
|
||||
|
||||
// Source represents a source of a JWT.
|
||||
type Source struct {
|
||||
// Type is the type of source. This could be "pat" etc.
|
||||
Type string `json:"type"`
|
||||
|
||||
// ID is the identifier to the source type. If "pat" then this will be the
|
||||
// ID of the PAT.
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// GetClaims returns claims from an access token without verification.
|
||||
func GetClaims(accessToken string) (Claims, error) {
|
||||
token, err := parseSigned(accessToken)
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
var claims Claims
|
||||
err = token.UnsafeClaimsWithoutVerification(&claims)
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// allowedSignatureAlgorithms is a list of allowed signature algorithms for JWTs.
|
||||
// We add all supported algorithms for Auth0, including with higher key lengths.
|
||||
// See auth0 docs: https://auth0.com/docs/get-started/applications/signing-algorithms
|
||||
var allowedSignatureAlgorithms = []jose.SignatureAlgorithm{
|
||||
jose.HS256,
|
||||
jose.HS384,
|
||||
jose.HS512,
|
||||
jose.RS256, // currently used for auth0
|
||||
jose.RS384,
|
||||
jose.RS512,
|
||||
jose.PS256,
|
||||
jose.PS384,
|
||||
jose.PS512,
|
||||
}
|
||||
|
||||
// parseSigned parses a JWT and returns the signature object or error. This does
|
||||
// not verify the validity of the JWT.
|
||||
func parseSigned(token string) (*jwt.JSONWebToken, error) {
|
||||
return jwt.ParseSigned(token, allowedSignatureAlgorithms)
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/oauth"
|
||||
"github.com/docker/cli/cli/internal/oauth/api"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/pkg/browser"
|
||||
)
|
||||
|
||||
// OAuthManager is the manager responsible for handling authentication
|
||||
// flows with the oauth tenant.
|
||||
type OAuthManager struct {
|
||||
store credentials.Store
|
||||
tenant string
|
||||
audience string
|
||||
clientID string
|
||||
api api.OAuthAPI
|
||||
openBrowser func(string) error
|
||||
}
|
||||
|
||||
// OAuthManagerOptions are the options used for New to create a new auth manager.
|
||||
type OAuthManagerOptions struct {
|
||||
Store credentials.Store
|
||||
Audience string
|
||||
ClientID string
|
||||
Scopes []string
|
||||
Tenant string
|
||||
DeviceName string
|
||||
OpenBrowser func(string) error
|
||||
}
|
||||
|
||||
func New(options OAuthManagerOptions) *OAuthManager {
|
||||
scopes := []string{"openid", "offline_access"}
|
||||
if len(options.Scopes) > 0 {
|
||||
scopes = options.Scopes
|
||||
}
|
||||
|
||||
openBrowser := options.OpenBrowser
|
||||
if openBrowser == nil {
|
||||
// Prevent errors from missing binaries (like xdg-open) from
|
||||
// cluttering the output. We can handle errors ourselves.
|
||||
browser.Stdout = io.Discard
|
||||
browser.Stderr = io.Discard
|
||||
openBrowser = browser.OpenURL
|
||||
}
|
||||
|
||||
return &OAuthManager{
|
||||
clientID: options.ClientID,
|
||||
audience: options.Audience,
|
||||
tenant: options.Tenant,
|
||||
store: options.Store,
|
||||
api: api.API{
|
||||
TenantURL: "https://" + options.Tenant,
|
||||
ClientID: options.ClientID,
|
||||
Scopes: scopes,
|
||||
},
|
||||
openBrowser: openBrowser,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrDeviceLoginStartFail = errors.New("failed to start device code flow login")
|
||||
|
||||
// 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.
|
||||
// After the user completes the browser login, LoginDevice uses the retrieved
|
||||
// tokens to create a Hub PAT which is returned to the caller.
|
||||
// The retrieved tokens are stored in the credentials store (under a separate
|
||||
// key), and the refresh token is concatenated with the client ID.
|
||||
func (m *OAuthManager) LoginDevice(ctx context.Context, w io.Writer) (*types.AuthConfig, error) {
|
||||
state, err := m.api.GetDeviceCode(ctx, m.audience)
|
||||
if err != nil {
|
||||
logrus.Debugf("failed to start device code login: %v", err)
|
||||
return nil, ErrDeviceLoginStartFail
|
||||
}
|
||||
|
||||
if state.UserCode == "" {
|
||||
logrus.Debugf("failed to start device code login: missing user code")
|
||||
return nil, ErrDeviceLoginStartFail
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w, aec.Bold.Apply("\nUSING WEB-BASED LOGIN"))
|
||||
|
||||
var out tui.Output
|
||||
switch stream := w.(type) {
|
||||
case *streams.Out:
|
||||
out = tui.NewOutput(stream)
|
||||
default:
|
||||
out = tui.NewOutput(streams.NewOut(w))
|
||||
}
|
||||
out.PrintNote("To sign in with credentials on the command line, use 'docker login -u <username>'\n")
|
||||
_, _ = fmt.Fprintf(w, "\nYour one-time device confirmation code is: "+aec.Bold.Apply("%s\n"), state.UserCode)
|
||||
_, _ = fmt.Fprintf(w, aec.Bold.Apply("Press ENTER")+" to open your browser or submit your device code here: "+aec.Underline.Apply("%s\n"), strings.Split(state.VerificationURI, "?")[0])
|
||||
|
||||
tokenResChan := make(chan api.TokenResponse)
|
||||
waitForTokenErrChan := make(chan error)
|
||||
go func() {
|
||||
tokenRes, err := m.api.WaitForDeviceToken(ctx, state)
|
||||
if err != nil {
|
||||
waitForTokenErrChan <- err
|
||||
return
|
||||
}
|
||||
tokenResChan <- tokenRes
|
||||
}()
|
||||
|
||||
go func() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
_, _ = reader.ReadString('\n')
|
||||
_ = m.openBrowser(state.VerificationURI)
|
||||
}()
|
||||
|
||||
_, _ = fmt.Fprint(w, "\nWaiting for authentication in the browser…\n")
|
||||
var tokenRes api.TokenResponse
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.New("login canceled")
|
||||
case err := <-waitForTokenErrChan:
|
||||
return nil, fmt.Errorf("failed waiting for authentication: %w", err)
|
||||
case tokenRes = <-tokenResChan:
|
||||
}
|
||||
|
||||
claims, err := oauth.GetClaims(tokenRes.AccessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token claims: %w", err)
|
||||
}
|
||||
|
||||
err = m.storeTokensInStore(tokenRes, claims.Domain.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to store tokens: %w", err)
|
||||
}
|
||||
|
||||
pat, err := m.api.GetAutoPAT(ctx, m.audience, tokenRes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.AuthConfig{
|
||||
Username: claims.Domain.Username,
|
||||
Password: pat,
|
||||
ServerAddress: registry.IndexServer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout fetches the refresh token from the store and revokes it
|
||||
// with the configured oauth tenant. The stored access and refresh
|
||||
// tokens are then erased from the store.
|
||||
// If the refresh token is not found in the store, an error is not
|
||||
// returned.
|
||||
func (m *OAuthManager) Logout(ctx context.Context) error {
|
||||
refreshConfig, err := m.store.Get(refreshTokenKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if refreshConfig.Password == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(refreshConfig.Password, "..")
|
||||
if len(parts) != 2 {
|
||||
// the token wasn't stored by the CLI, so don't revoke it
|
||||
// or erase it from the store/error
|
||||
return nil
|
||||
}
|
||||
// erase the token from the store first, that way
|
||||
// if the revoke fails, the user can try to logout again
|
||||
if err := m.eraseTokensFromStore(); err != nil {
|
||||
return fmt.Errorf("failed to erase tokens: %w", err)
|
||||
}
|
||||
if err := m.api.RevokeToken(ctx, parts[0]); err != nil {
|
||||
return fmt.Errorf("credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
accessTokenKey = registry.IndexServer + "access-token"
|
||||
refreshTokenKey = registry.IndexServer + "refresh-token"
|
||||
)
|
||||
|
||||
func (m *OAuthManager) storeTokensInStore(tokens api.TokenResponse, username string) error {
|
||||
return errors.Join(
|
||||
m.store.Store(types.AuthConfig{
|
||||
Username: username,
|
||||
Password: tokens.AccessToken,
|
||||
ServerAddress: accessTokenKey,
|
||||
}),
|
||||
m.store.Store(types.AuthConfig{
|
||||
Username: username,
|
||||
Password: tokens.RefreshToken + ".." + m.clientID,
|
||||
ServerAddress: refreshTokenKey,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *OAuthManager) eraseTokensFromStore() error {
|
||||
return errors.Join(
|
||||
m.store.Erase(accessTokenKey),
|
||||
m.store.Erase(refreshTokenKey),
|
||||
)
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/internal/oauth/api"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
//nolint:revive // ignore line-length-limit
|
||||
validToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InhYa3BCdDNyV3MyRy11YjlscEpncSJ9.eyJodHRwczovL2h1Yi5kb2NrZXIuY29tIjp7ImVtYWlsIjoiYm9ya0Bkb2NrZXIuY29tIiwic2Vzc2lvbl9pZCI6ImEtc2Vzc2lvbi1pZCIsInNvdXJjZSI6InNhbWxwIiwidXNlcm5hbWUiOiJib3JrISIsInV1aWQiOiIwMTIzLTQ1Njc4OSJ9LCJpc3MiOiJodHRwczovL2xvZ2luLmRvY2tlci5jb20vIiwic3ViIjoic2FtbHB8c2FtbHAtZG9ja2VyfGJvcmtAZG9ja2VyLmNvbSIsImF1ZCI6WyJodHRwczovL2F1ZGllbmNlLmNvbSJdLCJpYXQiOjE3MTk1MDI5MzksImV4cCI6MTcxOTUwNjUzOSwic2NvcGUiOiJvcGVuaWQgb2ZmbGluZV9hY2Nlc3MifQ.VUSp-9_SOvMPWJPRrSh7p4kSPoye4DA3kyd2I0TW0QtxYSRq7xCzNj0NC_ywlPlKBFBeXKm4mh93d1vBSh79I9Heq5tj0Fr4KH77U5xJRMEpjHqoT5jxMEU1hYXX92xctnagBMXxDvzUfu3Yf0tvYSA0RRoGbGTHfdYYRwOrGbwQ75Qg1dyIxUkwsG053eYX2XkmLGxymEMgIq_gWksgAamOc40_0OCdGr-MmDeD2HyGUa309aGltzQUw7Z0zG1AKSXy3WwfMHdWNFioTAvQphwEyY3US8ybSJi78upSFTjwUcryMeHUwQ3uV9PxwPMyPoYxo1izVB-OUJxM8RqEbg"
|
||||
)
|
||||
|
||||
// parsed token:
|
||||
// {
|
||||
// "https://hub.docker.com": {
|
||||
// "email": "bork@docker.com",
|
||||
// "session_id": "a-session-id",
|
||||
// "source": "samlp",
|
||||
// "username": "bork!",
|
||||
// "uuid": "0123-456789"
|
||||
// },
|
||||
// "iss": "https://login.docker.com/",
|
||||
// "sub": "samlp|samlp-docker|bork@docker.com",
|
||||
// "aud": [
|
||||
// "https://audience.com"
|
||||
// ],
|
||||
// "iat": 1719502939,
|
||||
// "exp": 1719506539,
|
||||
// "scope": "openid offline_access"
|
||||
// }
|
||||
|
||||
func TestLoginDevice(t *testing.T) {
|
||||
t.Run("valid token", func(t *testing.T) {
|
||||
expectedState := api.State{
|
||||
DeviceCode: "device-code",
|
||||
UserCode: "0123-4567",
|
||||
VerificationURI: "an-url",
|
||||
ExpiresIn: 300,
|
||||
}
|
||||
var receivedAudience string
|
||||
getDeviceToken := func(audience string) (api.State, error) {
|
||||
receivedAudience = audience
|
||||
return expectedState, nil
|
||||
}
|
||||
var receivedState api.State
|
||||
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
|
||||
receivedState = state
|
||||
return api.TokenResponse{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: "refresh-token",
|
||||
}, nil
|
||||
}
|
||||
var receivedAccessToken, getPatReceivedAudience string
|
||||
getAutoPat := func(audience string, res api.TokenResponse) (string, error) {
|
||||
receivedAccessToken = res.AccessToken
|
||||
getPatReceivedAudience = audience
|
||||
return "a-pat", nil
|
||||
}
|
||||
api := &testAPI{
|
||||
getDeviceToken: getDeviceToken,
|
||||
waitForDeviceToken: waitForDeviceToken,
|
||||
getAutoPAT: getAutoPat,
|
||||
}
|
||||
store := newStore(map[string]types.AuthConfig{})
|
||||
manager := OAuthManager{
|
||||
store: credentials.NewFileStore(store),
|
||||
audience: "https://hub.docker.com",
|
||||
api: api,
|
||||
openBrowser: func(url string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, receivedAudience, "https://hub.docker.com")
|
||||
assert.Equal(t, receivedState, expectedState)
|
||||
assert.DeepEqual(t, authConfig, &types.AuthConfig{
|
||||
Username: "bork!",
|
||||
Password: "a-pat",
|
||||
ServerAddress: "https://index.docker.io/v1/",
|
||||
})
|
||||
assert.Equal(t, receivedAccessToken, validToken)
|
||||
assert.Equal(t, getPatReceivedAudience, "https://hub.docker.com")
|
||||
})
|
||||
|
||||
t.Run("stores in cred store", func(t *testing.T) {
|
||||
getDeviceToken := func(audience string) (api.State, error) {
|
||||
return api.State{
|
||||
DeviceCode: "device-code",
|
||||
UserCode: "0123-4567",
|
||||
}, nil
|
||||
}
|
||||
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
|
||||
return api.TokenResponse{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: "refresh-token",
|
||||
}, nil
|
||||
}
|
||||
getAutoPAT := func(audience string, res api.TokenResponse) (string, error) {
|
||||
return "a-pat", nil
|
||||
}
|
||||
a := &testAPI{
|
||||
getDeviceToken: getDeviceToken,
|
||||
waitForDeviceToken: waitForDeviceToken,
|
||||
getAutoPAT: getAutoPAT,
|
||||
}
|
||||
store := newStore(map[string]types.AuthConfig{})
|
||||
manager := OAuthManager{
|
||||
clientID: "client-id",
|
||||
store: credentials.NewFileStore(store),
|
||||
api: a,
|
||||
openBrowser: func(url string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
authConfig, err := manager.LoginDevice(context.Background(), os.Stderr)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, authConfig.Password, "a-pat")
|
||||
assert.Equal(t, authConfig.Username, "bork!")
|
||||
|
||||
assert.Equal(t, len(store.configs), 2)
|
||||
assert.Equal(t, store.configs["https://index.docker.io/v1/access-token"].Password, validToken)
|
||||
assert.Equal(t, store.configs["https://index.docker.io/v1/refresh-token"].Password, "refresh-token..client-id")
|
||||
})
|
||||
|
||||
t.Run("timeout", func(t *testing.T) {
|
||||
getDeviceToken := func(audience string) (api.State, error) {
|
||||
return api.State{
|
||||
DeviceCode: "device-code",
|
||||
UserCode: "0123-4567",
|
||||
VerificationURI: "an-url",
|
||||
ExpiresIn: 300,
|
||||
}, nil
|
||||
}
|
||||
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
|
||||
return api.TokenResponse{}, api.ErrTimeout
|
||||
}
|
||||
a := &testAPI{
|
||||
getDeviceToken: getDeviceToken,
|
||||
waitForDeviceToken: waitForDeviceToken,
|
||||
}
|
||||
manager := OAuthManager{
|
||||
api: a,
|
||||
openBrowser: func(url string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := manager.LoginDevice(context.Background(), os.Stderr)
|
||||
assert.ErrorContains(t, err, "failed waiting for authentication: timed out waiting for device token")
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
getDeviceToken := func(audience string) (api.State, error) {
|
||||
return api.State{
|
||||
DeviceCode: "device-code",
|
||||
UserCode: "0123-4567",
|
||||
}, nil
|
||||
}
|
||||
waitForDeviceToken := func(state api.State) (api.TokenResponse, error) {
|
||||
// make sure that the context is cancelled before this returns
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return api.TokenResponse{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: "refresh-token",
|
||||
}, nil
|
||||
}
|
||||
a := &testAPI{
|
||||
getDeviceToken: getDeviceToken,
|
||||
waitForDeviceToken: waitForDeviceToken,
|
||||
}
|
||||
manager := OAuthManager{
|
||||
api: a,
|
||||
openBrowser: func(url string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
_, err := manager.LoginDevice(ctx, os.Stderr)
|
||||
assert.ErrorContains(t, err, "login canceled")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
t.Run("successfully revokes token", func(t *testing.T) {
|
||||
var receivedToken string
|
||||
a := &testAPI{
|
||||
revokeToken: func(token string) error {
|
||||
receivedToken = token
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := newStore(map[string]types.AuthConfig{
|
||||
"https://index.docker.io/v1/access-token": {
|
||||
Password: validToken,
|
||||
},
|
||||
"https://index.docker.io/v1/refresh-token": {
|
||||
Password: "a-refresh-token..client-id",
|
||||
},
|
||||
})
|
||||
manager := OAuthManager{
|
||||
store: credentials.NewFileStore(store),
|
||||
api: a,
|
||||
}
|
||||
|
||||
err := manager.Logout(context.Background())
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Equal(t, receivedToken, "a-refresh-token")
|
||||
assert.Equal(t, len(store.configs), 0)
|
||||
})
|
||||
|
||||
t.Run("error revoking token", func(t *testing.T) {
|
||||
a := &testAPI{
|
||||
revokeToken: func(token string) error {
|
||||
return errors.New("couldn't reach tenant")
|
||||
},
|
||||
}
|
||||
store := newStore(map[string]types.AuthConfig{
|
||||
"https://index.docker.io/v1/access-token": {
|
||||
Password: validToken,
|
||||
},
|
||||
"https://index.docker.io/v1/refresh-token": {
|
||||
Password: "a-refresh-token..client-id",
|
||||
},
|
||||
})
|
||||
manager := OAuthManager{
|
||||
store: credentials.NewFileStore(store),
|
||||
api: a,
|
||||
}
|
||||
|
||||
err := manager.Logout(context.Background())
|
||||
assert.ErrorContains(t, err, "credentials erased successfully, but there was a failure to revoke the OAuth refresh token with the tenant: couldn't reach tenant")
|
||||
|
||||
assert.Equal(t, len(store.configs), 0)
|
||||
})
|
||||
|
||||
t.Run("invalid refresh token", func(t *testing.T) {
|
||||
var triedRevoke bool
|
||||
a := &testAPI{
|
||||
revokeToken: func(token string) error {
|
||||
triedRevoke = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
store := newStore(map[string]types.AuthConfig{
|
||||
"https://index.docker.io/v1/access-token": {
|
||||
Password: validToken,
|
||||
},
|
||||
"https://index.docker.io/v1/refresh-token": {
|
||||
Password: "a-refresh-token-without-client-id",
|
||||
},
|
||||
})
|
||||
manager := OAuthManager{
|
||||
store: credentials.NewFileStore(store),
|
||||
api: a,
|
||||
}
|
||||
|
||||
err := manager.Logout(context.Background())
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, !triedRevoke)
|
||||
})
|
||||
|
||||
t.Run("no refresh token", func(t *testing.T) {
|
||||
a := &testAPI{}
|
||||
var triedRevoke bool
|
||||
revokeToken := func(token string) error {
|
||||
triedRevoke = true
|
||||
return nil
|
||||
}
|
||||
a.revokeToken = revokeToken
|
||||
store := newStore(map[string]types.AuthConfig{})
|
||||
manager := OAuthManager{
|
||||
store: credentials.NewFileStore(store),
|
||||
api: a,
|
||||
}
|
||||
|
||||
err := manager.Logout(context.Background())
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, !triedRevoke)
|
||||
})
|
||||
}
|
||||
|
||||
var _ api.OAuthAPI = &testAPI{}
|
||||
|
||||
type testAPI struct {
|
||||
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
|
||||
getAutoPAT func(audience string, res api.TokenResponse) (string, error)
|
||||
}
|
||||
|
||||
func (t *testAPI) GetDeviceCode(_ context.Context, audience string) (api.State, error) {
|
||||
if t.getDeviceToken != nil {
|
||||
return t.getDeviceToken(audience)
|
||||
}
|
||||
return api.State{}, nil
|
||||
}
|
||||
|
||||
func (t *testAPI) WaitForDeviceToken(_ context.Context, state api.State) (api.TokenResponse, error) {
|
||||
if t.waitForDeviceToken != nil {
|
||||
return t.waitForDeviceToken(state)
|
||||
}
|
||||
return api.TokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (t *testAPI) Refresh(_ context.Context, token string) (api.TokenResponse, error) {
|
||||
if t.refresh != nil {
|
||||
return t.refresh(token)
|
||||
}
|
||||
return api.TokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (t *testAPI) RevokeToken(_ context.Context, token string) error {
|
||||
if t.revokeToken != nil {
|
||||
return t.revokeToken(token)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testAPI) GetAutoPAT(_ context.Context, audience string, res api.TokenResponse) (string, error) {
|
||||
if t.getAutoPAT != nil {
|
||||
return t.getAutoPAT(audience, res)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
type fakeStore struct {
|
||||
configs map[string]types.AuthConfig
|
||||
}
|
||||
|
||||
func (*fakeStore) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) GetAuthConfigs() map[string]types.AuthConfig {
|
||||
return f.configs
|
||||
}
|
||||
|
||||
func (*fakeStore) GetFilename() string {
|
||||
return "/tmp/docker-fakestore"
|
||||
}
|
||||
|
||||
func newStore(auths map[string]types.AuthConfig) *fakeStore {
|
||||
return &fakeStore{configs: auths}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/cli/version"
|
||||
)
|
||||
|
||||
const (
|
||||
audience = "https://hub.docker.com"
|
||||
tenant = "login.docker.com"
|
||||
clientID = "L4v0dmlNBpYUjGGab0C2JtgTgXr1Qz4d"
|
||||
)
|
||||
|
||||
func NewManager(store credentials.Store) *OAuthManager {
|
||||
cliVersion := strings.ReplaceAll(version.Version, ".", "_")
|
||||
options := OAuthManagerOptions{
|
||||
Store: store,
|
||||
Audience: audience,
|
||||
ClientID: clientID,
|
||||
Tenant: tenant,
|
||||
DeviceName: fmt.Sprintf("docker-cli:%s:%s-%s", cliVersion, runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
return New(options)
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
|
||||
Reference in New Issue
Block a user