2016-12-25 14:31:52 -05:00
|
|
|
package credentials
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2017-10-15 15:39:56 -04:00
|
|
|
"github.com/docker/cli/cli/config/types"
|
2016-12-25 14:31:52 -05:00
|
|
|
"github.com/docker/docker-credential-helpers/client"
|
|
|
|
"github.com/docker/docker-credential-helpers/credentials"
|
2017-03-09 13:23:45 -05:00
|
|
|
"github.com/pkg/errors"
|
2020-02-22 12:12:14 -05:00
|
|
|
"gotest.tools/v3/assert"
|
|
|
|
is "gotest.tools/v3/assert/cmp"
|
2016-12-25 14:31:52 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
validServerAddress = "https://index.docker.io/v1"
|
|
|
|
validServerAddress2 = "https://example.com:5002"
|
|
|
|
invalidServerAddress = "https://foobar.example.com"
|
|
|
|
missingCredsAddress = "https://missing.docker.io/v1"
|
|
|
|
)
|
|
|
|
|
2017-03-09 13:23:45 -05:00
|
|
|
var errCommandExited = errors.Errorf("exited 1")
|
2016-12-25 14:31:52 -05:00
|
|
|
|
|
|
|
// mockCommand simulates interactions between the docker client and a remote
|
|
|
|
// credentials helper.
|
|
|
|
// Unit tests inject this mocked command into the remote to control execution.
|
|
|
|
type mockCommand struct {
|
|
|
|
arg string
|
|
|
|
input io.Reader
|
|
|
|
}
|
|
|
|
|
|
|
|
// Output returns responses from the remote credentials helper.
|
|
|
|
// It mocks those responses based in the input in the mock.
|
|
|
|
func (m *mockCommand) Output() ([]byte, error) {
|
2022-02-25 08:36:33 -05:00
|
|
|
in, err := io.ReadAll(m.input)
|
2016-12-25 14:31:52 -05:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
inS := string(in)
|
|
|
|
|
|
|
|
switch m.arg {
|
|
|
|
case "erase":
|
|
|
|
switch inS {
|
|
|
|
case validServerAddress:
|
|
|
|
return nil, nil
|
|
|
|
default:
|
|
|
|
return []byte("program failed"), errCommandExited
|
|
|
|
}
|
|
|
|
case "get":
|
|
|
|
switch inS {
|
|
|
|
case validServerAddress:
|
|
|
|
return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
|
|
|
case validServerAddress2:
|
|
|
|
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
|
|
|
case missingCredsAddress:
|
|
|
|
return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited
|
|
|
|
case invalidServerAddress:
|
|
|
|
return []byte("program failed"), errCommandExited
|
|
|
|
}
|
|
|
|
case "store":
|
|
|
|
var c credentials.Credentials
|
|
|
|
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
|
|
|
if err != nil {
|
|
|
|
return []byte("program failed"), errCommandExited
|
|
|
|
}
|
|
|
|
switch c.ServerURL {
|
|
|
|
case validServerAddress:
|
|
|
|
return nil, nil
|
|
|
|
default:
|
|
|
|
return []byte("program failed"), errCommandExited
|
|
|
|
}
|
|
|
|
case "list":
|
|
|
|
return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>")), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited
|
|
|
|
}
|
|
|
|
|
|
|
|
// Input sets the input to send to a remote credentials helper.
|
|
|
|
func (m *mockCommand) Input(in io.Reader) {
|
|
|
|
m.input = in
|
|
|
|
}
|
|
|
|
|
|
|
|
func mockCommandFn(args ...string) client.Program {
|
|
|
|
return &mockCommand{
|
|
|
|
arg: args[0],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreAddCredentials(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(make(map[string]types.AuthConfig))
|
2016-12-25 14:31:52 -05:00
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
2017-06-21 16:47:06 -04:00
|
|
|
auth := types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
Username: "foo",
|
|
|
|
Password: "bar",
|
|
|
|
Email: "foo@example.com",
|
|
|
|
ServerAddress: validServerAddress,
|
|
|
|
}
|
2017-06-21 16:47:06 -04:00
|
|
|
err := s.Store(auth)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 1))
|
2016-12-25 14:31:52 -05:00
|
|
|
|
2017-06-21 16:47:06 -04:00
|
|
|
actual, ok := f.GetAuthConfigs()[validServerAddress]
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.Check(t, ok)
|
2017-06-21 16:47:06 -04:00
|
|
|
expected := types.AuthConfig{
|
|
|
|
Email: auth.Email,
|
|
|
|
ServerAddress: auth.ServerAddress,
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreAddInvalidCredentials(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(make(map[string]types.AuthConfig))
|
2016-12-25 14:31:52 -05:00
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
err := s.Store(types.AuthConfig{
|
|
|
|
Username: "foo",
|
|
|
|
Password: "bar",
|
|
|
|
Email: "foo@example.com",
|
|
|
|
ServerAddress: invalidServerAddress,
|
|
|
|
})
|
2018-03-06 14:03:47 -05:00
|
|
|
assert.ErrorContains(t, err, "program failed")
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreGet(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
2017-06-21 16:47:06 -04:00
|
|
|
actual, err := s.Get(validServerAddress)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
2016-12-25 14:31:52 -05:00
|
|
|
|
2017-06-21 16:47:06 -04:00
|
|
|
expected := types.AuthConfig{
|
Fix setting ServerAddress property in NativeStore
This will return the ServerAddress property when using the NativeStore.
This happens when you use docker credential helpers, not the credential
store.
The reason this fix is needed is because it needs to be propagated
properly down towards `moby/moby` project in the following logic:
```golang
func authorizationCredsFromAuthConfig(authConfig registrytypes.AuthConfig) docker.AuthorizerOpt {
cfgHost := registry.ConvertToHostname(authConfig.ServerAddress)
if cfgHost == "" || cfgHost == registry.IndexHostname {
cfgHost = registry.DefaultRegistryHost
}
return docker.WithAuthCreds(func(host string) (string, string, error) {
if cfgHost != host {
logrus.WithFields(logrus.Fields{
"host": host,
"cfgHost": cfgHost,
}).Warn("Host doesn't match")
return "", "", nil
}
if authConfig.IdentityToken != "" {
return "", authConfig.IdentityToken, nil
}
return authConfig.Username, authConfig.Password, nil
})
}
```
This logic resides in the following file :
`daemon/containerd/resolver.go` .
In the case when using the containerd storage feature when setting the
`cfgHost` variable from the `authConfig.ServerAddress` it will always be
empty. Since it will never be returned from the NativeStore currently.
Therefore Docker Hub images will work fine, but anything else will fail
since the `cfgHost` will always be the `registry.DefaultRegistryHost`.
Signed-off-by: Eric Bode <eric.bode@foundries.io>
(cherry picked from commit b24e7f85a488da13dc2d5be87acdaab2652634bb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-11-10 16:38:10 -05:00
|
|
|
Username: "foo",
|
|
|
|
Password: "bar",
|
|
|
|
Email: "foo@example.com",
|
|
|
|
ServerAddress: validServerAddress,
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreGetIdentityToken(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress2: {
|
|
|
|
Email: "foo@example2.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
2017-06-21 16:47:06 -04:00
|
|
|
actual, err := s.Get(validServerAddress2)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
2016-12-25 14:31:52 -05:00
|
|
|
|
2017-06-21 16:47:06 -04:00
|
|
|
expected := types.AuthConfig{
|
|
|
|
IdentityToken: "abcd1234",
|
|
|
|
Email: "foo@example2.com",
|
Fix setting ServerAddress property in NativeStore
This will return the ServerAddress property when using the NativeStore.
This happens when you use docker credential helpers, not the credential
store.
The reason this fix is needed is because it needs to be propagated
properly down towards `moby/moby` project in the following logic:
```golang
func authorizationCredsFromAuthConfig(authConfig registrytypes.AuthConfig) docker.AuthorizerOpt {
cfgHost := registry.ConvertToHostname(authConfig.ServerAddress)
if cfgHost == "" || cfgHost == registry.IndexHostname {
cfgHost = registry.DefaultRegistryHost
}
return docker.WithAuthCreds(func(host string) (string, string, error) {
if cfgHost != host {
logrus.WithFields(logrus.Fields{
"host": host,
"cfgHost": cfgHost,
}).Warn("Host doesn't match")
return "", "", nil
}
if authConfig.IdentityToken != "" {
return "", authConfig.IdentityToken, nil
}
return authConfig.Username, authConfig.Password, nil
})
}
```
This logic resides in the following file :
`daemon/containerd/resolver.go` .
In the case when using the containerd storage feature when setting the
`cfgHost` variable from the `authConfig.ServerAddress` it will always be
empty. Since it will never be returned from the NativeStore currently.
Therefore Docker Hub images will work fine, but anything else will fail
since the `cfgHost` will always be the `registry.DefaultRegistryHost`.
Signed-off-by: Eric Bode <eric.bode@foundries.io>
(cherry picked from commit b24e7f85a488da13dc2d5be87acdaab2652634bb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2023-11-10 16:38:10 -05:00
|
|
|
ServerAddress: validServerAddress2,
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.Check(t, is.DeepEqual(expected, actual))
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreGetAll(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
as, err := s.GetAll()
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.Check(t, is.Len(as, 2))
|
2016-12-25 14:31:52 -05:00
|
|
|
|
|
|
|
if as[validServerAddress].Username != "foo" {
|
|
|
|
t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username)
|
|
|
|
}
|
|
|
|
if as[validServerAddress].Password != "bar" {
|
|
|
|
t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password)
|
|
|
|
}
|
|
|
|
if as[validServerAddress].IdentityToken != "" {
|
|
|
|
t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken)
|
|
|
|
}
|
|
|
|
if as[validServerAddress].Email != "foo@example.com" {
|
|
|
|
t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email)
|
|
|
|
}
|
|
|
|
if as[validServerAddress2].Username != "" {
|
|
|
|
t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username)
|
|
|
|
}
|
|
|
|
if as[validServerAddress2].Password != "" {
|
|
|
|
t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password)
|
|
|
|
}
|
|
|
|
if as[validServerAddress2].IdentityToken != "abcd1234" {
|
|
|
|
t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken)
|
|
|
|
}
|
|
|
|
if as[validServerAddress2].Email != "" {
|
|
|
|
t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreGetMissingCredentials(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
_, err := s.Get(missingCredsAddress)
|
2018-03-06 14:44:13 -05:00
|
|
|
assert.NilError(t, err)
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreGetInvalidAddress(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
_, err := s.Get(invalidServerAddress)
|
2018-03-06 14:03:47 -05:00
|
|
|
assert.ErrorContains(t, err, "program failed")
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreErase(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
err := s.Erase(validServerAddress)
|
2018-03-05 18:53:52 -05:00
|
|
|
assert.NilError(t, err)
|
|
|
|
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestNativeStoreEraseInvalidAddress(t *testing.T) {
|
2017-06-21 16:47:06 -04:00
|
|
|
f := newStore(map[string]types.AuthConfig{
|
2016-12-25 14:31:52 -05:00
|
|
|
validServerAddress: {
|
|
|
|
Email: "foo@example.com",
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
s := &nativeStore{
|
|
|
|
programFunc: mockCommandFn,
|
|
|
|
fileStore: NewFileStore(f),
|
|
|
|
}
|
|
|
|
err := s.Erase(invalidServerAddress)
|
2018-03-06 14:03:47 -05:00
|
|
|
assert.ErrorContains(t, err, "program failed")
|
2016-12-25 14:31:52 -05:00
|
|
|
}
|