authz_plugin_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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/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) func() {
  48. ctrl = &authorizationController{}
  49. teardown := setupTest(t)
  50. err := os.MkdirAll("/etc/docker/plugins", 0755)
  51. assert.NilError(t, err)
  52. fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
  53. err = ioutil.WriteFile(fileName, []byte(server.URL), 0644)
  54. assert.NilError(t, err)
  55. return func() {
  56. err := os.RemoveAll("/etc/docker/plugins")
  57. assert.NilError(t, err)
  58. teardown()
  59. ctrl = nil
  60. }
  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 TestAuthZPluginAllowRequest(t *testing.T) {
  72. defer setupTestV1(t)()
  73. ctrl.reqRes.Allow = true
  74. ctrl.resRes.Allow = true
  75. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin)
  76. client, err := d.NewClient()
  77. assert.NilError(t, err)
  78. ctx := context.Background()
  79. // Ensure command successful
  80. cID := container.Run(t, ctx, client)
  81. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  82. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
  83. _, err = client.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. client, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
  108. assert.NilError(t, err)
  109. _, err = client.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. client, err := d.NewClient()
  130. assert.NilError(t, err)
  131. // Ensure command is blocked
  132. _, err = client.ServerVersion(context.Background())
  133. assert.Assert(t, err != nil)
  134. assert.Equal(t, 1, ctrl.versionReqCount)
  135. assert.Equal(t, 0, ctrl.versionResCount)
  136. // Ensure unauthorized message appears in response
  137. assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  138. }
  139. // TestAuthZPluginAPIDenyResponse validates that when authorization
  140. // plugin deny the request, the status code is forbidden
  141. func TestAuthZPluginAPIDenyResponse(t *testing.T) {
  142. defer setupTestV1(t)()
  143. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  144. ctrl.reqRes.Allow = false
  145. ctrl.resRes.Msg = unauthorizedMessage
  146. daemonURL, err := url.Parse(d.Sock())
  147. assert.NilError(t, err)
  148. conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
  149. assert.NilError(t, err)
  150. client := httputil.NewClientConn(conn, nil)
  151. req, err := http.NewRequest("GET", "/version", nil)
  152. assert.NilError(t, err)
  153. resp, err := client.Do(req)
  154. assert.NilError(t, err)
  155. assert.DeepEqual(t, http.StatusForbidden, resp.StatusCode)
  156. }
  157. func TestAuthZPluginDenyResponse(t *testing.T) {
  158. defer setupTestV1(t)()
  159. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  160. ctrl.reqRes.Allow = true
  161. ctrl.resRes.Allow = false
  162. ctrl.resRes.Msg = unauthorizedMessage
  163. client, err := d.NewClient()
  164. assert.NilError(t, err)
  165. // Ensure command is blocked
  166. _, err = client.ServerVersion(context.Background())
  167. assert.Assert(t, err != nil)
  168. assert.Equal(t, 1, ctrl.versionReqCount)
  169. assert.Equal(t, 1, ctrl.versionResCount)
  170. // Ensure unauthorized message appears in response
  171. assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  172. }
  173. // TestAuthZPluginAllowEventStream verifies event stream propagates
  174. // correctly after request pass through by the authorization plugin
  175. func TestAuthZPluginAllowEventStream(t *testing.T) {
  176. skip.If(t, testEnv.DaemonInfo.OSType != "linux")
  177. skip.If(t, testEnv.DaemonInfo.OSType == "windows")
  178. defer setupTestV1(t)()
  179. ctrl.reqRes.Allow = true
  180. ctrl.resRes.Allow = true
  181. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin)
  182. client, err := d.NewClient()
  183. assert.NilError(t, err)
  184. ctx := context.Background()
  185. startTime := strconv.FormatInt(systemTime(t, client, testEnv).Unix(), 10)
  186. events, errs, cancel := systemEventsSince(client, startTime)
  187. defer cancel()
  188. // Create a container and wait for the creation events
  189. cID := container.Run(t, ctx, client)
  190. for i := 0; i < 100; i++ {
  191. c, err := client.ContainerInspect(ctx, cID)
  192. assert.NilError(t, err)
  193. if c.State.Running {
  194. break
  195. }
  196. if i == 99 {
  197. t.Fatal("Container didn't run within 10s")
  198. }
  199. time.Sleep(100 * time.Millisecond)
  200. }
  201. created := false
  202. started := false
  203. for !created && !started {
  204. select {
  205. case event := <-events:
  206. if event.Type == eventtypes.ContainerEventType && event.Actor.ID == cID {
  207. if event.Action == "create" {
  208. created = true
  209. }
  210. if event.Action == "start" {
  211. started = true
  212. }
  213. }
  214. case err := <-errs:
  215. if err == io.EOF {
  216. t.Fatal("premature end of event stream")
  217. }
  218. assert.NilError(t, err)
  219. case <-time.After(30 * time.Second):
  220. // Fail the test
  221. t.Fatal("event stream timeout")
  222. }
  223. }
  224. // Ensure both events and container endpoints are passed to the
  225. // authorization plugin
  226. assertURIRecorded(t, ctrl.requestsURIs, "/events")
  227. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  228. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
  229. }
  230. func systemTime(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
  231. if testEnv.IsLocalDaemon() {
  232. return time.Now()
  233. }
  234. ctx := context.Background()
  235. info, err := client.Info(ctx)
  236. assert.NilError(t, err)
  237. dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
  238. assert.NilError(t, err, "invalid time format in GET /info response")
  239. return dt
  240. }
  241. func systemEventsSince(client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) {
  242. eventOptions := types.EventsOptions{
  243. Since: since,
  244. }
  245. ctx, cancel := context.WithCancel(context.Background())
  246. events, errs := client.Events(ctx, eventOptions)
  247. return events, errs, cancel
  248. }
  249. func TestAuthZPluginErrorResponse(t *testing.T) {
  250. defer setupTestV1(t)()
  251. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  252. ctrl.reqRes.Allow = true
  253. ctrl.resRes.Err = errorMessage
  254. client, err := d.NewClient()
  255. assert.NilError(t, err)
  256. // Ensure command is blocked
  257. _, err = client.ServerVersion(context.Background())
  258. assert.Assert(t, err != nil)
  259. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
  260. }
  261. func TestAuthZPluginErrorRequest(t *testing.T) {
  262. defer setupTestV1(t)()
  263. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  264. ctrl.reqRes.Err = errorMessage
  265. client, err := d.NewClient()
  266. assert.NilError(t, err)
  267. // Ensure command is blocked
  268. _, err = client.ServerVersion(context.Background())
  269. assert.Assert(t, err != nil)
  270. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
  271. }
  272. func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
  273. defer setupTestV1(t)()
  274. d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  275. ctrl.reqRes.Allow = true
  276. ctrl.resRes.Allow = true
  277. client, err := d.NewClient()
  278. assert.NilError(t, err)
  279. _, err = client.ServerVersion(context.Background())
  280. assert.NilError(t, err)
  281. // assert plugin is only called once..
  282. assert.Equal(t, 1, ctrl.versionReqCount)
  283. assert.Equal(t, 1, ctrl.versionResCount)
  284. }
  285. func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
  286. defer setupTestV1(t)()
  287. ctrl.reqRes.Allow = true
  288. ctrl.resRes.Allow = true
  289. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  290. client, err := d.NewClient()
  291. assert.NilError(t, err)
  292. ctx := context.Background()
  293. tmp, err := ioutil.TempDir("", "test-authz-load-import")
  294. assert.NilError(t, err)
  295. defer os.RemoveAll(tmp)
  296. savedImagePath := filepath.Join(tmp, "save.tar")
  297. err = imageSave(client, savedImagePath, "busybox")
  298. assert.NilError(t, err)
  299. err = imageLoad(client, savedImagePath)
  300. assert.NilError(t, err)
  301. exportedImagePath := filepath.Join(tmp, "export.tar")
  302. cID := container.Run(t, ctx, client)
  303. responseReader, err := client.ContainerExport(context.Background(), cID)
  304. assert.NilError(t, err)
  305. defer responseReader.Close()
  306. file, err := os.Create(exportedImagePath)
  307. assert.NilError(t, err)
  308. defer file.Close()
  309. _, err = io.Copy(file, responseReader)
  310. assert.NilError(t, err)
  311. err = imageImport(client, exportedImagePath)
  312. assert.NilError(t, err)
  313. }
  314. func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
  315. defer setupTestV1(t)()
  316. ctrl.reqRes.Allow = true
  317. ctrl.resRes.Allow = true
  318. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  319. dir, err := ioutil.TempDir("", t.Name())
  320. assert.Assert(t, err)
  321. defer os.RemoveAll(dir)
  322. f, err := ioutil.TempFile(dir, "send")
  323. assert.Assert(t, err)
  324. defer f.Close()
  325. buf := make([]byte, 1024)
  326. fileSize := len(buf) * 1024 * 10
  327. for written := 0; written < fileSize; {
  328. n, err := f.Write(buf)
  329. assert.Assert(t, err)
  330. written += n
  331. }
  332. ctx := context.Background()
  333. client, err := d.NewClient()
  334. assert.Assert(t, err)
  335. cID := container.Run(t, ctx, client)
  336. defer client.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
  337. _, err = f.Seek(0, io.SeekStart)
  338. assert.Assert(t, err)
  339. srcInfo, err := archive.CopyInfoSourcePath(f.Name(), false)
  340. assert.Assert(t, err)
  341. srcArchive, err := archive.TarResource(srcInfo)
  342. assert.Assert(t, err)
  343. defer srcArchive.Close()
  344. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
  345. assert.Assert(t, err)
  346. err = client.CopyToContainer(ctx, cID, dstDir, preparedArchive, types.CopyToContainerOptions{})
  347. assert.Assert(t, err)
  348. rdr, _, err := client.CopyFromContainer(ctx, cID, "/test")
  349. assert.Assert(t, err)
  350. _, err = io.Copy(ioutil.Discard, rdr)
  351. assert.Assert(t, err)
  352. }
  353. func imageSave(client client.APIClient, path, image string) error {
  354. ctx := context.Background()
  355. responseReader, err := client.ImageSave(ctx, []string{image})
  356. if err != nil {
  357. return err
  358. }
  359. defer responseReader.Close()
  360. file, err := os.Create(path)
  361. if err != nil {
  362. return err
  363. }
  364. defer file.Close()
  365. _, err = io.Copy(file, responseReader)
  366. return err
  367. }
  368. func imageLoad(client client.APIClient, path string) error {
  369. file, err := os.Open(path)
  370. if err != nil {
  371. return err
  372. }
  373. defer file.Close()
  374. quiet := true
  375. ctx := context.Background()
  376. response, err := client.ImageLoad(ctx, file, quiet)
  377. if err != nil {
  378. return err
  379. }
  380. defer response.Body.Close()
  381. return nil
  382. }
  383. func imageImport(client client.APIClient, path string) error {
  384. file, err := os.Open(path)
  385. if err != nil {
  386. return err
  387. }
  388. defer file.Close()
  389. options := types.ImageImportOptions{}
  390. ref := ""
  391. source := types.ImageImportSource{
  392. Source: file,
  393. SourceName: "-",
  394. }
  395. ctx := context.Background()
  396. responseReader, err := client.ImageImport(ctx, source, ref, options)
  397. if err != nil {
  398. return err
  399. }
  400. defer responseReader.Close()
  401. return nil
  402. }
  403. func TestAuthZPluginHeader(t *testing.T) {
  404. defer setupTestV1(t)()
  405. ctrl.reqRes.Allow = true
  406. ctrl.resRes.Allow = true
  407. d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin)
  408. daemonURL, err := url.Parse(d.Sock())
  409. assert.NilError(t, err)
  410. conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
  411. assert.NilError(t, err)
  412. client := httputil.NewClientConn(conn, nil)
  413. req, err := http.NewRequest("GET", "/version", nil)
  414. assert.NilError(t, err)
  415. resp, err := client.Do(req)
  416. assert.NilError(t, err)
  417. assert.Equal(t, "application/json", resp.Header["Content-Type"][0])
  418. }
  419. // assertURIRecorded verifies that the given URI was sent and recorded
  420. // in the authz plugin
  421. func assertURIRecorded(t *testing.T, uris []string, uri string) {
  422. var found bool
  423. for _, u := range uris {
  424. if strings.Contains(u, uri) {
  425. found = true
  426. break
  427. }
  428. }
  429. if !found {
  430. t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
  431. }
  432. }