oci_windows_test.go 10 KB

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