authz_plugin_test.go 14 KB

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