mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			313 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			313 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package daemon
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"gotest.tools/v3/fs"
 | 
						|
 | 
						|
	containertypes "github.com/docker/docker/api/types/container"
 | 
						|
	"github.com/docker/docker/container"
 | 
						|
	swarmagent "github.com/docker/swarmkit/agent"
 | 
						|
	swarmapi "github.com/docker/swarmkit/api"
 | 
						|
	specs "github.com/opencontainers/runtime-spec/specs-go"
 | 
						|
	"golang.org/x/sys/windows/registry"
 | 
						|
	"gotest.tools/v3/assert"
 | 
						|
)
 | 
						|
 | 
						|
func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
 | 
						|
	// we need a temp directory to act as the daemon's root
 | 
						|
	tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
 | 
						|
	defer func() {
 | 
						|
		assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
 | 
						|
	}()
 | 
						|
 | 
						|
	daemon := &Daemon{
 | 
						|
		root: tmpDaemonRoot,
 | 
						|
	}
 | 
						|
 | 
						|
	t.Run("it does nothing if there are no security options", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
 | 
						|
		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
 | 
						|
		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	dummyContainerID := "dummy-container-ID"
 | 
						|
	containerFactory := func(secOpt string) *container.Container {
 | 
						|
		if !strings.Contains(secOpt, "=") {
 | 
						|
			secOpt = "credentialspec=" + secOpt
 | 
						|
		}
 | 
						|
		return &container.Container{
 | 
						|
			ID: dummyContainerID,
 | 
						|
			HostConfig: &containertypes.HostConfig{
 | 
						|
				SecurityOpt: []string{secOpt},
 | 
						|
			},
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
 | 
						|
	dummyCredFileContents := `{"We don't need no": "education"}`
 | 
						|
 | 
						|
	t.Run("happy path with a 'file://' option", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		// let's render a dummy cred file
 | 
						|
		err := os.Mkdir(credSpecsDir, os.ModePerm)
 | 
						|
		assert.NilError(t, err)
 | 
						|
		dummyCredFileName := "dummy-cred-spec.json"
 | 
						|
		dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
 | 
						|
		err = os.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0644)
 | 
						|
		defer func() {
 | 
						|
			assert.NilError(t, os.Remove(dummyCredFilePath))
 | 
						|
		}()
 | 
						|
		assert.NilError(t, err)
 | 
						|
 | 
						|
		err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
 | 
						|
		if assert.Check(t, spec.Windows != nil) {
 | 
						|
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
 | 
						|
		assert.ErrorContains(t, err, "invalid credential spec - file:// path cannot be absolute")
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
 | 
						|
		assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec - file:// path must be under %s", credSpecsDir))
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
 | 
						|
		assert.ErrorContains(t, err, fmt.Sprintf("credential spec for container %s could not be read from file", dummyContainerID))
 | 
						|
		assert.ErrorContains(t, err, "The system cannot find")
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("happy path with a 'registry://' option", func(t *testing.T) {
 | 
						|
		valueName := "my-cred-spec"
 | 
						|
		key := &dummyRegistryKey{
 | 
						|
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
 | 
						|
				assert.Equal(t, valueName, name)
 | 
						|
				return dummyCredFileContents, 0, nil
 | 
						|
			},
 | 
						|
		}
 | 
						|
		defer setRegistryOpenKeyFunc(t, key)()
 | 
						|
 | 
						|
		spec := &specs.Spec{}
 | 
						|
		assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))
 | 
						|
 | 
						|
		if assert.Check(t, spec.Windows != nil) {
 | 
						|
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
 | 
						|
		}
 | 
						|
		assert.Check(t, key.closed)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
 | 
						|
		dummyError := fmt.Errorf("dummy error")
 | 
						|
		defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()
 | 
						|
 | 
						|
		spec := &specs.Spec{}
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
 | 
						|
		assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
 | 
						|
		valueName := "my-cred-spec"
 | 
						|
		key := &dummyRegistryKey{
 | 
						|
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
 | 
						|
				assert.Equal(t, valueName, name)
 | 
						|
				return "", 0, registry.ErrNotExist
 | 
						|
			},
 | 
						|
		}
 | 
						|
		defer setRegistryOpenKeyFunc(t, key)()
 | 
						|
 | 
						|
		spec := &specs.Spec{}
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
 | 
						|
		assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))
 | 
						|
 | 
						|
		assert.Check(t, key.closed)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
 | 
						|
		dummyError := fmt.Errorf("dummy error")
 | 
						|
		valueName := "my-cred-spec"
 | 
						|
		key := &dummyRegistryKey{
 | 
						|
			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
 | 
						|
				assert.Equal(t, valueName, name)
 | 
						|
				return "", 0, dummyError
 | 
						|
			},
 | 
						|
		}
 | 
						|
		defer setRegistryOpenKeyFunc(t, key)()
 | 
						|
 | 
						|
		spec := &specs.Spec{}
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
 | 
						|
		assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))
 | 
						|
 | 
						|
		assert.Check(t, key.closed)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("happy path with a 'config://' option", func(t *testing.T) {
 | 
						|
		configID := "my-cred-spec"
 | 
						|
 | 
						|
		dependencyManager := swarmagent.NewDependencyManager(nil)
 | 
						|
		dependencyManager.Configs().Add(swarmapi.Config{
 | 
						|
			ID: configID,
 | 
						|
			Spec: swarmapi.ConfigSpec{
 | 
						|
				Data: []byte(dummyCredFileContents),
 | 
						|
			},
 | 
						|
		})
 | 
						|
 | 
						|
		task := &swarmapi.Task{
 | 
						|
			Spec: swarmapi.TaskSpec{
 | 
						|
				Runtime: &swarmapi.TaskSpec_Container{
 | 
						|
					Container: &swarmapi.ContainerSpec{
 | 
						|
						Configs: []*swarmapi.ConfigReference{
 | 
						|
							{
 | 
						|
								ConfigID: configID,
 | 
						|
							},
 | 
						|
						},
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		}
 | 
						|
 | 
						|
		cntr := containerFactory("config://" + configID)
 | 
						|
		cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)
 | 
						|
 | 
						|
		spec := &specs.Spec{}
 | 
						|
		err := daemon.setWindowsCredentialSpec(cntr, spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
 | 
						|
		if assert.Check(t, spec.Windows != nil) {
 | 
						|
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("using a 'config://' option on a container not managed by swarmkit is not allowed, and results in a generic error message to hide that purely internal API", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
 | 
						|
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("happy path with a 'raw://' option", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
 | 
						|
		if assert.Check(t, spec.Windows != nil) {
 | 
						|
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("it's not case sensitive in the option names", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
 | 
						|
		assert.NilError(t, err)
 | 
						|
 | 
						|
		if assert.Check(t, spec.Windows != nil) {
 | 
						|
			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
 | 
						|
		}
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("it rejects unknown options", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
 | 
						|
		assert.ErrorContains(t, err, "security option not supported: credentialspe")
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
 | 
						|
		spec := &specs.Spec{}
 | 
						|
 | 
						|
		err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
 | 
						|
		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
 | 
						|
 | 
						|
		assert.Check(t, spec.Windows == nil)
 | 
						|
	})
 | 
						|
 | 
						|
	for _, option := range []string{"file", "registry", "config", "raw"} {
 | 
						|
		t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
 | 
						|
			spec := &specs.Spec{}
 | 
						|
 | 
						|
			err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
 | 
						|
			assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
 | 
						|
 | 
						|
			assert.Check(t, spec.Windows == nil)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
/* Helpers below */
 | 
						|
 | 
						|
type dummyRegistryKey struct {
 | 
						|
	getStringValueFunc func(name string) (val string, valtype uint32, err error)
 | 
						|
	closed             bool
 | 
						|
}
 | 
						|
 | 
						|
func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
 | 
						|
	return k.getStringValueFunc(name)
 | 
						|
}
 | 
						|
 | 
						|
func (k *dummyRegistryKey) Close() error {
 | 
						|
	k.closed = true
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
 | 
						|
// to be called to revert the change when done with testing.
 | 
						|
func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
 | 
						|
	previousRegistryOpenKeyFunc := registryOpenKeyFunc
 | 
						|
 | 
						|
	registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
 | 
						|
		// this should always be called with exactly the same arguments
 | 
						|
		assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
 | 
						|
		assert.Equal(t, credentialSpecRegistryLocation, path)
 | 
						|
		assert.Equal(t, uint32(registry.QUERY_VALUE), access)
 | 
						|
 | 
						|
		if len(err) > 0 {
 | 
						|
			return nil, err[0]
 | 
						|
		}
 | 
						|
		return key, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return func() {
 | 
						|
		registryOpenKeyFunc = previousRegistryOpenKeyFunc
 | 
						|
	}
 | 
						|
}
 |