authz_plugin_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. //go:build !windows
  2. package authz // import "github.com/docker/docker/integration/plugin/authz"
  3. import (
  4. "context"
  5. "fmt"
  6. "io"
  7. "net"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path/filepath"
  12. "strconv"
  13. "strings"
  14. "testing"
  15. "time"
  16. "github.com/docker/docker/api/types"
  17. containertypes "github.com/docker/docker/api/types/container"
  18. eventtypes "github.com/docker/docker/api/types/events"
  19. "github.com/docker/docker/client"
  20. "github.com/docker/docker/integration/internal/container"
  21. "github.com/docker/docker/pkg/archive"
  22. "github.com/docker/docker/pkg/authorization"
  23. "github.com/docker/docker/testutil/environment"
  24. "github.com/docker/go-connections/sockets"
  25. "gotest.tools/v3/assert"
  26. "gotest.tools/v3/skip"
  27. )
  28. const (
  29. testAuthZPlugin = "authzplugin"
  30. unauthorizedMessage = "User unauthorized authz plugin"
  31. errorMessage = "something went wrong..."
  32. serverVersionAPI = "/version"
  33. )
  34. var (
  35. alwaysAllowed = []string{"/_ping", "/info"}
  36. ctrl *authorizationController
  37. )
  38. type authorizationController struct {
  39. reqRes authorization.Response // reqRes holds the plugin response to the initial client request
  40. resRes authorization.Response // resRes holds the plugin response to the daemon response
  41. versionReqCount int // versionReqCount counts the number of requests to the server version API endpoint
  42. versionResCount int // versionResCount counts the number of responses from the server version API endpoint
  43. requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller
  44. reqUser string
  45. resUser string
  46. }
  47. func setupTestV1(t *testing.T) context.Context {
  48. ctx := setupTest(t)
  49. ctrl = &authorizationController{}
  50. err := os.MkdirAll("/etc/docker/plugins", 0o755)
  51. assert.NilError(t, err)
  52. fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
  53. err = os.WriteFile(fileName, []byte(server.URL), 0o644)
  54. assert.NilError(t, err)
  55. t.Cleanup(func() {
  56. err := os.RemoveAll("/etc/docker/plugins")
  57. assert.NilError(t, err)
  58. ctrl = nil
  59. })
  60. return ctx
  61. }
  62. // check for always allowed endpoints to not inhibit test framework functions
  63. func isAllowed(reqURI string) bool {
  64. for _, endpoint := range alwaysAllowed {
  65. if strings.HasSuffix(reqURI, endpoint) {
  66. return true
  67. }
  68. }
  69. return false
  70. }
  71. func socketHTTPClient(u *url.URL) (*http.Client, error) {
  72. transport := &http.Transport{}
  73. err := sockets.ConfigureTransport(transport, u.Scheme, u.Path)
  74. if err != nil {
  75. return nil, err
  76. }
  77. return &http.Client{
  78. Transport: transport,
  79. }, nil
  80. }
  81. func TestAuthZPluginAllowRequest(t *testing.T) {
  82. ctx := setupTestV1(t)
  83. ctrl.reqRes.Allow = true
  84. ctrl.resRes.Allow = true
  85. d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin)
  86. c := d.NewClientT(t)
  87. // Ensure command successful
  88. cID := container.Run(ctx, t, c)
  89. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  90. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
  91. _, err := c.ServerVersion(ctx)
  92. assert.NilError(t, err)
  93. assert.Equal(t, 1, ctrl.versionReqCount)
  94. assert.Equal(t, 1, ctrl.versionResCount)
  95. }
  96. func TestAuthZPluginTLS(t *testing.T) {
  97. ctx := setupTestV1(t)
  98. const (
  99. testDaemonHTTPSAddr = "tcp://localhost:4271"
  100. cacertPath = "../../testdata/https/ca.pem"
  101. serverCertPath = "../../testdata/https/server-cert.pem"
  102. serverKeyPath = "../../testdata/https/server-key.pem"
  103. clientCertPath = "../../testdata/https/client-cert.pem"
  104. clientKeyPath = "../../testdata/https/client-key.pem"
  105. )
  106. d.Start(t,
  107. "--authorization-plugin="+testAuthZPlugin,
  108. "--tlsverify",
  109. "--tlscacert", cacertPath,
  110. "--tlscert", serverCertPath,
  111. "--tlskey", serverKeyPath,
  112. "-H", testDaemonHTTPSAddr)
  113. ctrl.reqRes.Allow = true
  114. ctrl.resRes.Allow = true
  115. c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
  116. assert.NilError(t, err)
  117. _, err = c.ServerVersion(ctx)
  118. assert.NilError(t, err)
  119. assert.Equal(t, "client", ctrl.reqUser)
  120. assert.Equal(t, "client", ctrl.resUser)
  121. }
  122. func newTLSAPIClient(host, cacertPath, certPath, keyPath string) (client.APIClient, error) {
  123. dialer := &net.Dialer{
  124. KeepAlive: 30 * time.Second,
  125. Timeout: 30 * time.Second,
  126. }
  127. return client.NewClientWithOpts(
  128. client.WithTLSClientConfig(cacertPath, certPath, keyPath),
  129. client.WithDialContext(dialer.DialContext),
  130. client.WithHost(host))
  131. }
  132. func TestAuthZPluginDenyRequest(t *testing.T) {
  133. ctx := setupTestV1(t)
  134. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  135. ctrl.reqRes.Allow = false
  136. ctrl.reqRes.Msg = unauthorizedMessage
  137. c := d.NewClientT(t)
  138. // Ensure command is blocked
  139. _, err := c.ServerVersion(ctx)
  140. assert.Assert(t, err != nil)
  141. assert.Equal(t, 1, ctrl.versionReqCount)
  142. assert.Equal(t, 0, ctrl.versionResCount)
  143. // Ensure unauthorized message appears in response
  144. assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  145. }
  146. // TestAuthZPluginAPIDenyResponse validates that when authorization
  147. // plugin deny the request, the status code is forbidden
  148. func TestAuthZPluginAPIDenyResponse(t *testing.T) {
  149. ctx := setupTestV1(t)
  150. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  151. ctrl.reqRes.Allow = false
  152. ctrl.resRes.Msg = unauthorizedMessage
  153. daemonURL, err := url.Parse(d.Sock())
  154. assert.NilError(t, err)
  155. socketClient, err := socketHTTPClient(daemonURL)
  156. assert.NilError(t, err)
  157. req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/version", nil)
  158. assert.NilError(t, err)
  159. req.URL.Scheme = "http"
  160. req.URL.Host = client.DummyHost
  161. resp, err := socketClient.Do(req)
  162. assert.NilError(t, err)
  163. assert.DeepEqual(t, http.StatusForbidden, resp.StatusCode)
  164. }
  165. func TestAuthZPluginDenyResponse(t *testing.T) {
  166. ctx := setupTestV1(t)
  167. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  168. ctrl.reqRes.Allow = true
  169. ctrl.resRes.Allow = false
  170. ctrl.resRes.Msg = unauthorizedMessage
  171. c := d.NewClientT(t)
  172. // Ensure command is blocked
  173. _, err := c.ServerVersion(ctx)
  174. assert.Assert(t, err != nil)
  175. assert.Equal(t, 1, ctrl.versionReqCount)
  176. assert.Equal(t, 1, ctrl.versionResCount)
  177. // Ensure unauthorized message appears in response
  178. assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  179. }
  180. // TestAuthZPluginAllowEventStream verifies event stream propagates
  181. // correctly after request pass through by the authorization plugin
  182. func TestAuthZPluginAllowEventStream(t *testing.T) {
  183. skip.If(t, testEnv.DaemonInfo.OSType != "linux")
  184. skip.If(t, testEnv.DaemonInfo.OSType == "windows")
  185. ctx := setupTestV1(t)
  186. ctrl.reqRes.Allow = true
  187. ctrl.resRes.Allow = true
  188. d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin)
  189. c := d.NewClientT(t)
  190. startTime := strconv.FormatInt(systemTime(ctx, t, c, testEnv).Unix(), 10)
  191. events, errs, cancel := systemEventsSince(ctx, c, startTime)
  192. defer cancel()
  193. // Create a container and wait for the creation events
  194. cID := container.Run(ctx, t, c)
  195. created := false
  196. started := false
  197. for !created && !started {
  198. select {
  199. case event := <-events:
  200. if event.Type == eventtypes.ContainerEventType && event.Actor.ID == cID {
  201. if event.Action == eventtypes.ActionCreate {
  202. created = true
  203. }
  204. if event.Action == eventtypes.ActionStart {
  205. started = true
  206. }
  207. }
  208. case err := <-errs:
  209. if err == io.EOF {
  210. t.Fatal("premature end of event stream")
  211. }
  212. assert.NilError(t, err)
  213. case <-time.After(30 * time.Second):
  214. // Fail the test
  215. t.Fatal("event stream timeout")
  216. }
  217. }
  218. // Ensure both events and container endpoints are passed to the
  219. // authorization plugin
  220. assertURIRecorded(t, ctrl.requestsURIs, "/events")
  221. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  222. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
  223. }
  224. func systemTime(ctx context.Context, t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
  225. if testEnv.IsLocalDaemon() {
  226. return time.Now()
  227. }
  228. info, err := client.Info(ctx)
  229. assert.NilError(t, err)
  230. dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
  231. assert.NilError(t, err, "invalid time format in GET /info response")
  232. return dt
  233. }
  234. func systemEventsSince(ctx context.Context, client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) {
  235. eventOptions := types.EventsOptions{
  236. Since: since,
  237. }
  238. ctx, cancel := context.WithCancel(ctx)
  239. events, errs := client.Events(ctx, eventOptions)
  240. return events, errs, cancel
  241. }
  242. func TestAuthZPluginErrorResponse(t *testing.T) {
  243. ctx := setupTestV1(t)
  244. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  245. ctrl.reqRes.Allow = true
  246. ctrl.resRes.Err = errorMessage
  247. c := d.NewClientT(t)
  248. // Ensure command is blocked
  249. _, err := c.ServerVersion(ctx)
  250. assert.Assert(t, err != nil)
  251. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
  252. }
  253. func TestAuthZPluginErrorRequest(t *testing.T) {
  254. ctx := setupTestV1(t)
  255. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  256. ctrl.reqRes.Err = errorMessage
  257. c := d.NewClientT(t)
  258. // Ensure command is blocked
  259. _, err := c.ServerVersion(ctx)
  260. assert.Assert(t, err != nil)
  261. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
  262. }
  263. func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
  264. ctx := setupTestV1(t)
  265. d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  266. ctrl.reqRes.Allow = true
  267. ctrl.resRes.Allow = true
  268. c := d.NewClientT(t)
  269. _, err := c.ServerVersion(ctx)
  270. assert.NilError(t, err)
  271. // assert plugin is only called once..
  272. assert.Equal(t, 1, ctrl.versionReqCount)
  273. assert.Equal(t, 1, ctrl.versionResCount)
  274. }
  275. func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
  276. ctx := setupTestV1(t)
  277. ctrl.reqRes.Allow = true
  278. ctrl.resRes.Allow = true
  279. d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  280. c := d.NewClientT(t)
  281. tmp, err := os.MkdirTemp("", "test-authz-load-import")
  282. assert.NilError(t, err)
  283. defer os.RemoveAll(tmp)
  284. savedImagePath := filepath.Join(tmp, "save.tar")
  285. err = imageSave(ctx, c, savedImagePath, "busybox")
  286. assert.NilError(t, err)
  287. err = imageLoad(ctx, c, savedImagePath)
  288. assert.NilError(t, err)
  289. exportedImagePath := filepath.Join(tmp, "export.tar")
  290. cID := container.Run(ctx, t, c)
  291. responseReader, err := c.ContainerExport(ctx, cID)
  292. assert.NilError(t, err)
  293. defer responseReader.Close()
  294. file, err := os.Create(exportedImagePath)
  295. assert.NilError(t, err)
  296. defer file.Close()
  297. _, err = io.Copy(file, responseReader)
  298. assert.NilError(t, err)
  299. err = imageImport(ctx, c, exportedImagePath)
  300. assert.NilError(t, err)
  301. }
  302. func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
  303. ctx := setupTestV1(t)
  304. ctrl.reqRes.Allow = true
  305. ctrl.resRes.Allow = true
  306. d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  307. dir, err := os.MkdirTemp("", t.Name())
  308. assert.NilError(t, err)
  309. defer os.RemoveAll(dir)
  310. f, err := os.CreateTemp(dir, "send")
  311. assert.NilError(t, err)
  312. defer f.Close()
  313. buf := make([]byte, 1024)
  314. fileSize := len(buf) * 1024 * 10
  315. for written := 0; written < fileSize; {
  316. n, err := f.Write(buf)
  317. assert.NilError(t, err)
  318. written += n
  319. }
  320. c := d.NewClientT(t)
  321. cID := container.Run(ctx, t, c)
  322. defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
  323. _, err = f.Seek(0, io.SeekStart)
  324. assert.NilError(t, err)
  325. srcInfo, err := archive.CopyInfoSourcePath(f.Name(), false)
  326. assert.NilError(t, err)
  327. srcArchive, err := archive.TarResource(srcInfo)
  328. assert.NilError(t, err)
  329. defer srcArchive.Close()
  330. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
  331. assert.NilError(t, err)
  332. err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, types.CopyToContainerOptions{})
  333. assert.NilError(t, err)
  334. rdr, _, err := c.CopyFromContainer(ctx, cID, "/test")
  335. assert.NilError(t, err)
  336. _, err = io.Copy(io.Discard, rdr)
  337. assert.NilError(t, err)
  338. }
  339. func imageSave(ctx context.Context, client client.APIClient, path, image string) error {
  340. responseReader, err := client.ImageSave(ctx, []string{image})
  341. if err != nil {
  342. return err
  343. }
  344. defer responseReader.Close()
  345. file, err := os.Create(path)
  346. if err != nil {
  347. return err
  348. }
  349. defer file.Close()
  350. _, err = io.Copy(file, responseReader)
  351. return err
  352. }
  353. func imageLoad(ctx context.Context, client client.APIClient, path string) error {
  354. file, err := os.Open(path)
  355. if err != nil {
  356. return err
  357. }
  358. defer file.Close()
  359. quiet := true
  360. response, err := client.ImageLoad(ctx, file, quiet)
  361. if err != nil {
  362. return err
  363. }
  364. defer response.Body.Close()
  365. return nil
  366. }
  367. func imageImport(ctx context.Context, client client.APIClient, path string) error {
  368. file, err := os.Open(path)
  369. if err != nil {
  370. return err
  371. }
  372. defer file.Close()
  373. options := types.ImageImportOptions{}
  374. ref := ""
  375. source := types.ImageImportSource{
  376. Source: file,
  377. SourceName: "-",
  378. }
  379. responseReader, err := client.ImageImport(ctx, source, ref, options)
  380. if err != nil {
  381. return err
  382. }
  383. defer responseReader.Close()
  384. return nil
  385. }
  386. func TestAuthZPluginHeader(t *testing.T) {
  387. ctx := setupTestV1(t)
  388. ctrl.reqRes.Allow = true
  389. ctrl.resRes.Allow = true
  390. d.StartWithBusybox(ctx, t, "--debug", "--authorization-plugin="+testAuthZPlugin)
  391. daemonURL, err := url.Parse(d.Sock())
  392. assert.NilError(t, err)
  393. socketClient, err := socketHTTPClient(daemonURL)
  394. assert.NilError(t, err)
  395. req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/version", nil)
  396. assert.NilError(t, err)
  397. req.URL.Scheme = "http"
  398. req.URL.Host = client.DummyHost
  399. resp, err := socketClient.Do(req)
  400. assert.NilError(t, err)
  401. assert.Equal(t, "application/json", resp.Header["Content-Type"][0])
  402. }
  403. // assertURIRecorded verifies that the given URI was sent and recorded
  404. // in the authz plugin
  405. func assertURIRecorded(t *testing.T, uris []string, uri string) {
  406. var found bool
  407. for _, u := range uris {
  408. if strings.Contains(u, uri) {
  409. found = true
  410. break
  411. }
  412. }
  413. if !found {
  414. t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
  415. }
  416. }