github.com/docker/docker@v299999999.0.0-20200612211812-aaf470eca7b5+incompatible/daemon/oci_windows_test.go (about)

     1  package daemon
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"testing"
    10  
    11  	"gotest.tools/v3/fs"
    12  
    13  	containertypes "github.com/docker/docker/api/types/container"
    14  	"github.com/docker/docker/container"
    15  	swarmagent "github.com/docker/swarmkit/agent"
    16  	swarmapi "github.com/docker/swarmkit/api"
    17  	specs "github.com/opencontainers/runtime-spec/specs-go"
    18  	"golang.org/x/sys/windows/registry"
    19  	"gotest.tools/v3/assert"
    20  )
    21  
    22  func TestSetWindowsCredentialSpecInSpec(t *testing.T) {
    23  	// we need a temp directory to act as the daemon's root
    24  	tmpDaemonRoot := fs.NewDir(t, t.Name()).Path()
    25  	defer func() {
    26  		assert.NilError(t, os.RemoveAll(tmpDaemonRoot))
    27  	}()
    28  
    29  	daemon := &Daemon{
    30  		root: tmpDaemonRoot,
    31  	}
    32  
    33  	t.Run("it does nothing if there are no security options", func(t *testing.T) {
    34  		spec := &specs.Spec{}
    35  
    36  		err := daemon.setWindowsCredentialSpec(&container.Container{}, spec)
    37  		assert.NilError(t, err)
    38  		assert.Check(t, spec.Windows == nil)
    39  
    40  		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{}}, spec)
    41  		assert.NilError(t, err)
    42  		assert.Check(t, spec.Windows == nil)
    43  
    44  		err = daemon.setWindowsCredentialSpec(&container.Container{HostConfig: &containertypes.HostConfig{SecurityOpt: []string{}}}, spec)
    45  		assert.NilError(t, err)
    46  		assert.Check(t, spec.Windows == nil)
    47  	})
    48  
    49  	dummyContainerID := "dummy-container-ID"
    50  	containerFactory := func(secOpt string) *container.Container {
    51  		if !strings.Contains(secOpt, "=") {
    52  			secOpt = "credentialspec=" + secOpt
    53  		}
    54  		return &container.Container{
    55  			ID: dummyContainerID,
    56  			HostConfig: &containertypes.HostConfig{
    57  				SecurityOpt: []string{secOpt},
    58  			},
    59  		}
    60  	}
    61  
    62  	credSpecsDir := filepath.Join(tmpDaemonRoot, credentialSpecFileLocation)
    63  	dummyCredFileContents := `{"We don't need no": "education"}`
    64  
    65  	t.Run("happy path with a 'file://' option", func(t *testing.T) {
    66  		spec := &specs.Spec{}
    67  
    68  		// let's render a dummy cred file
    69  		err := os.Mkdir(credSpecsDir, os.ModePerm)
    70  		assert.NilError(t, err)
    71  		dummyCredFileName := "dummy-cred-spec.json"
    72  		dummyCredFilePath := filepath.Join(credSpecsDir, dummyCredFileName)
    73  		err = ioutil.WriteFile(dummyCredFilePath, []byte(dummyCredFileContents), 0644)
    74  		defer func() {
    75  			assert.NilError(t, os.Remove(dummyCredFilePath))
    76  		}()
    77  		assert.NilError(t, err)
    78  
    79  		err = daemon.setWindowsCredentialSpec(containerFactory("file://"+dummyCredFileName), spec)
    80  		assert.NilError(t, err)
    81  
    82  		if assert.Check(t, spec.Windows != nil) {
    83  			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
    84  		}
    85  	})
    86  
    87  	t.Run("it's not allowed to use a 'file://' option with an absolute path", func(t *testing.T) {
    88  		spec := &specs.Spec{}
    89  
    90  		err := daemon.setWindowsCredentialSpec(containerFactory(`file://C:\path\to\my\credspec.json`), spec)
    91  		assert.ErrorContains(t, err, "invalid credential spec - file:// path cannot be absolute")
    92  
    93  		assert.Check(t, spec.Windows == nil)
    94  	})
    95  
    96  	t.Run("it's not allowed to use a 'file://' option breaking out of the cred specs' directory", func(t *testing.T) {
    97  		spec := &specs.Spec{}
    98  
    99  		err := daemon.setWindowsCredentialSpec(containerFactory(`file://..\credspec.json`), spec)
   100  		assert.ErrorContains(t, err, fmt.Sprintf("invalid credential spec - file:// path must be under %s", credSpecsDir))
   101  
   102  		assert.Check(t, spec.Windows == nil)
   103  	})
   104  
   105  	t.Run("when using a 'file://' option pointing to a file that doesn't exist, it fails gracefully", func(t *testing.T) {
   106  		spec := &specs.Spec{}
   107  
   108  		err := daemon.setWindowsCredentialSpec(containerFactory("file://i-dont-exist.json"), spec)
   109  		assert.ErrorContains(t, err, fmt.Sprintf("credential spec for container %s could not be read from file", dummyContainerID))
   110  		assert.ErrorContains(t, err, "The system cannot find")
   111  
   112  		assert.Check(t, spec.Windows == nil)
   113  	})
   114  
   115  	t.Run("happy path with a 'registry://' option", func(t *testing.T) {
   116  		valueName := "my-cred-spec"
   117  		key := &dummyRegistryKey{
   118  			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
   119  				assert.Equal(t, valueName, name)
   120  				return dummyCredFileContents, 0, nil
   121  			},
   122  		}
   123  		defer setRegistryOpenKeyFunc(t, key)()
   124  
   125  		spec := &specs.Spec{}
   126  		assert.NilError(t, daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec))
   127  
   128  		if assert.Check(t, spec.Windows != nil) {
   129  			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
   130  		}
   131  		assert.Check(t, key.closed)
   132  	})
   133  
   134  	t.Run("when using a 'registry://' option and opening the registry key fails, it fails gracefully", func(t *testing.T) {
   135  		dummyError := fmt.Errorf("dummy error")
   136  		defer setRegistryOpenKeyFunc(t, &dummyRegistryKey{}, dummyError)()
   137  
   138  		spec := &specs.Spec{}
   139  		err := daemon.setWindowsCredentialSpec(containerFactory("registry://my-cred-spec"), spec)
   140  		assert.ErrorContains(t, err, fmt.Sprintf("registry key %s could not be opened: %v", credentialSpecRegistryLocation, dummyError))
   141  
   142  		assert.Check(t, spec.Windows == nil)
   143  	})
   144  
   145  	t.Run("when using a 'registry://' option pointing to a value that doesn't exist, it fails gracefully", func(t *testing.T) {
   146  		valueName := "my-cred-spec"
   147  		key := &dummyRegistryKey{
   148  			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
   149  				assert.Equal(t, valueName, name)
   150  				return "", 0, registry.ErrNotExist
   151  			},
   152  		}
   153  		defer setRegistryOpenKeyFunc(t, key)()
   154  
   155  		spec := &specs.Spec{}
   156  		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
   157  		assert.ErrorContains(t, err, fmt.Sprintf("registry credential spec %q for container %s was not found", valueName, dummyContainerID))
   158  
   159  		assert.Check(t, key.closed)
   160  	})
   161  
   162  	t.Run("when using a 'registry://' option and reading the registry value fails, it fails gracefully", func(t *testing.T) {
   163  		dummyError := fmt.Errorf("dummy error")
   164  		valueName := "my-cred-spec"
   165  		key := &dummyRegistryKey{
   166  			getStringValueFunc: func(name string) (val string, valtype uint32, err error) {
   167  				assert.Equal(t, valueName, name)
   168  				return "", 0, dummyError
   169  			},
   170  		}
   171  		defer setRegistryOpenKeyFunc(t, key)()
   172  
   173  		spec := &specs.Spec{}
   174  		err := daemon.setWindowsCredentialSpec(containerFactory("registry://"+valueName), spec)
   175  		assert.ErrorContains(t, err, fmt.Sprintf("error reading credential spec %q from registry for container %s: %v", valueName, dummyContainerID, dummyError))
   176  
   177  		assert.Check(t, key.closed)
   178  	})
   179  
   180  	t.Run("happy path with a 'config://' option", func(t *testing.T) {
   181  		configID := "my-cred-spec"
   182  
   183  		dependencyManager := swarmagent.NewDependencyManager()
   184  		dependencyManager.Configs().Add(swarmapi.Config{
   185  			ID: configID,
   186  			Spec: swarmapi.ConfigSpec{
   187  				Data: []byte(dummyCredFileContents),
   188  			},
   189  		})
   190  
   191  		task := &swarmapi.Task{
   192  			Spec: swarmapi.TaskSpec{
   193  				Runtime: &swarmapi.TaskSpec_Container{
   194  					Container: &swarmapi.ContainerSpec{
   195  						Configs: []*swarmapi.ConfigReference{
   196  							{
   197  								ConfigID: configID,
   198  							},
   199  						},
   200  					},
   201  				},
   202  			},
   203  		}
   204  
   205  		cntr := containerFactory("config://" + configID)
   206  		cntr.DependencyStore = swarmagent.Restrict(dependencyManager, task)
   207  
   208  		spec := &specs.Spec{}
   209  		err := daemon.setWindowsCredentialSpec(cntr, spec)
   210  		assert.NilError(t, err)
   211  
   212  		if assert.Check(t, spec.Windows != nil) {
   213  			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
   214  		}
   215  	})
   216  
   217  	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) {
   218  		spec := &specs.Spec{}
   219  
   220  		err := daemon.setWindowsCredentialSpec(containerFactory("config://whatever"), spec)
   221  		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
   222  
   223  		assert.Check(t, spec.Windows == nil)
   224  	})
   225  
   226  	t.Run("happy path with a 'raw://' option", func(t *testing.T) {
   227  		spec := &specs.Spec{}
   228  
   229  		err := daemon.setWindowsCredentialSpec(containerFactory("raw://"+dummyCredFileContents), spec)
   230  		assert.NilError(t, err)
   231  
   232  		if assert.Check(t, spec.Windows != nil) {
   233  			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
   234  		}
   235  	})
   236  
   237  	t.Run("it's not case sensitive in the option names", func(t *testing.T) {
   238  		spec := &specs.Spec{}
   239  
   240  		err := daemon.setWindowsCredentialSpec(containerFactory("CreDENtiaLSPeC=rAw://"+dummyCredFileContents), spec)
   241  		assert.NilError(t, err)
   242  
   243  		if assert.Check(t, spec.Windows != nil) {
   244  			assert.Equal(t, dummyCredFileContents, spec.Windows.CredentialSpec)
   245  		}
   246  	})
   247  
   248  	t.Run("it rejects unknown options", func(t *testing.T) {
   249  		spec := &specs.Spec{}
   250  
   251  		err := daemon.setWindowsCredentialSpec(containerFactory("credentialspe=config://whatever"), spec)
   252  		assert.ErrorContains(t, err, "security option not supported: credentialspe")
   253  
   254  		assert.Check(t, spec.Windows == nil)
   255  	})
   256  
   257  	t.Run("it rejects unsupported credentialspec options", func(t *testing.T) {
   258  		spec := &specs.Spec{}
   259  
   260  		err := daemon.setWindowsCredentialSpec(containerFactory("idontexist://whatever"), spec)
   261  		assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
   262  
   263  		assert.Check(t, spec.Windows == nil)
   264  	})
   265  
   266  	for _, option := range []string{"file", "registry", "config", "raw"} {
   267  		t.Run(fmt.Sprintf("it rejects empty values for %s", option), func(t *testing.T) {
   268  			spec := &specs.Spec{}
   269  
   270  			err := daemon.setWindowsCredentialSpec(containerFactory(option+"://"), spec)
   271  			assert.Equal(t, errInvalidCredentialSpecSecOpt, err)
   272  
   273  			assert.Check(t, spec.Windows == nil)
   274  		})
   275  	}
   276  }
   277  
   278  /* Helpers below */
   279  
   280  type dummyRegistryKey struct {
   281  	getStringValueFunc func(name string) (val string, valtype uint32, err error)
   282  	closed             bool
   283  }
   284  
   285  func (k *dummyRegistryKey) GetStringValue(name string) (val string, valtype uint32, err error) {
   286  	return k.getStringValueFunc(name)
   287  }
   288  
   289  func (k *dummyRegistryKey) Close() error {
   290  	k.closed = true
   291  	return nil
   292  }
   293  
   294  // setRegistryOpenKeyFunc replaces the registryOpenKeyFunc package variable, and returns a function
   295  // to be called to revert the change when done with testing.
   296  func setRegistryOpenKeyFunc(t *testing.T, key *dummyRegistryKey, err ...error) func() {
   297  	previousRegistryOpenKeyFunc := registryOpenKeyFunc
   298  
   299  	registryOpenKeyFunc = func(baseKey registry.Key, path string, access uint32) (registryKey, error) {
   300  		// this should always be called with exactly the same arguments
   301  		assert.Equal(t, registry.LOCAL_MACHINE, baseKey)
   302  		assert.Equal(t, credentialSpecRegistryLocation, path)
   303  		assert.Equal(t, uint32(registry.QUERY_VALUE), access)
   304  
   305  		if len(err) > 0 {
   306  			return nil, err[0]
   307  		}
   308  		return key, nil
   309  	}
   310  
   311  	return func() {
   312  		registryOpenKeyFunc = previousRegistryOpenKeyFunc
   313  	}
   314  }