DockerCLI/cli/config/credentials/native_store_test.go

279 lines
7.4 KiB
Go
Raw Normal View History

package credentials
import (
"encoding/json"
"fmt"
"io"
"strings"
"testing"
"github.com/docker/cli/cli/config/types"
"github.com/docker/docker-credential-helpers/client"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/pkg/errors"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
const (
validServerAddress = "https://index.docker.io/v1"
validServerAddress2 = "https://example.com:5002"
invalidServerAddress = "https://foobar.example.com"
missingCredsAddress = "https://missing.docker.io/v1"
)
var errCommandExited = errors.Errorf("exited 1")
// 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) {
in, err := io.ReadAll(m.input)
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) {
f := newStore(make(map[string]types.AuthConfig))
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
auth := types.AuthConfig{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
ServerAddress: validServerAddress,
}
err := s.Store(auth)
assert.NilError(t, err)
assert.Check(t, is.Len(f.GetAuthConfigs(), 1))
actual, ok := f.GetAuthConfigs()[validServerAddress]
assert.Check(t, ok)
expected := types.AuthConfig{
Email: auth.Email,
ServerAddress: auth.ServerAddress,
}
assert.Check(t, is.DeepEqual(expected, actual))
}
func TestNativeStoreAddInvalidCredentials(t *testing.T) {
f := newStore(make(map[string]types.AuthConfig))
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
err := s.Store(types.AuthConfig{
Username: "foo",
Password: "bar",
Email: "foo@example.com",
ServerAddress: invalidServerAddress,
})
assert.ErrorContains(t, err, "program failed")
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
}
func TestNativeStoreGet(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
actual, err := s.Get(validServerAddress)
assert.NilError(t, err)
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,
}
assert.Check(t, is.DeepEqual(expected, actual))
}
func TestNativeStoreGetIdentityToken(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress2: {
Email: "foo@example2.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
actual, err := s.Get(validServerAddress2)
assert.NilError(t, err)
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,
}
assert.Check(t, is.DeepEqual(expected, actual))
}
func TestNativeStoreGetAll(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
as, err := s.GetAll()
assert.NilError(t, err)
assert.Check(t, is.Len(as, 2))
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) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
_, err := s.Get(missingCredsAddress)
assert.NilError(t, err)
}
func TestNativeStoreGetInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
_, err := s.Get(invalidServerAddress)
assert.ErrorContains(t, err, "program failed")
}
func TestNativeStoreErase(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
err := s.Erase(validServerAddress)
assert.NilError(t, err)
assert.Check(t, is.Len(f.GetAuthConfigs(), 0))
}
func TestNativeStoreEraseInvalidAddress(t *testing.T) {
f := newStore(map[string]types.AuthConfig{
validServerAddress: {
Email: "foo@example.com",
},
})
s := &nativeStore{
programFunc: mockCommandFn,
fileStore: NewFileStore(f),
}
err := s.Erase(invalidServerAddress)
assert.ErrorContains(t, err, "program failed")
}