authz_plugin_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  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. "github.com/gotestyourself/gotestyourself/assert"
  26. "github.com/gotestyourself/gotestyourself/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.WithDialer(dialer),
  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.IfCondition(t, testEnv.DaemonInfo.OSType != "linux")
  177. defer setupTestV1(t)()
  178. ctrl.reqRes.Allow = true
  179. ctrl.resRes.Allow = true
  180. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin)
  181. client, err := d.NewClient()
  182. assert.NilError(t, err)
  183. ctx := context.Background()
  184. startTime := strconv.FormatInt(systemTime(t, client, testEnv).Unix(), 10)
  185. events, errs, cancel := systemEventsSince(client, startTime)
  186. defer cancel()
  187. // Create a container and wait for the creation events
  188. cID := container.Run(t, ctx, client)
  189. for i := 0; i < 100; i++ {
  190. c, err := client.ContainerInspect(ctx, cID)
  191. assert.NilError(t, err)
  192. if c.State.Running {
  193. break
  194. }
  195. if i == 99 {
  196. t.Fatal("Container didn't run within 10s")
  197. }
  198. time.Sleep(100 * time.Millisecond)
  199. }
  200. created := false
  201. started := false
  202. for !created && !started {
  203. select {
  204. case event := <-events:
  205. if event.Type == eventtypes.ContainerEventType && event.Actor.ID == cID {
  206. if event.Action == "create" {
  207. created = true
  208. }
  209. if event.Action == "start" {
  210. started = true
  211. }
  212. }
  213. case err := <-errs:
  214. if err == io.EOF {
  215. t.Fatal("premature end of event stream")
  216. }
  217. assert.NilError(t, err)
  218. case <-time.After(30 * time.Second):
  219. // Fail the test
  220. t.Fatal("event stream timeout")
  221. }
  222. }
  223. // Ensure both events and container endpoints are passed to the
  224. // authorization plugin
  225. assertURIRecorded(t, ctrl.requestsURIs, "/events")
  226. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  227. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
  228. }
  229. func systemTime(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
  230. if testEnv.IsLocalDaemon() {
  231. return time.Now()
  232. }
  233. ctx := context.Background()
  234. info, err := client.Info(ctx)
  235. assert.NilError(t, err)
  236. dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
  237. assert.NilError(t, err, "invalid time format in GET /info response")
  238. return dt
  239. }
  240. func systemEventsSince(client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) {
  241. eventOptions := types.EventsOptions{
  242. Since: since,
  243. }
  244. ctx, cancel := context.WithCancel(context.Background())
  245. events, errs := client.Events(ctx, eventOptions)
  246. return events, errs, cancel
  247. }
  248. func TestAuthZPluginErrorResponse(t *testing.T) {
  249. defer setupTestV1(t)()
  250. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  251. ctrl.reqRes.Allow = true
  252. ctrl.resRes.Err = errorMessage
  253. client, err := d.NewClient()
  254. assert.NilError(t, err)
  255. // Ensure command is blocked
  256. _, err = client.ServerVersion(context.Background())
  257. assert.Assert(t, err != nil)
  258. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
  259. }
  260. func TestAuthZPluginErrorRequest(t *testing.T) {
  261. defer setupTestV1(t)()
  262. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  263. ctrl.reqRes.Err = errorMessage
  264. client, err := d.NewClient()
  265. assert.NilError(t, err)
  266. // Ensure command is blocked
  267. _, err = client.ServerVersion(context.Background())
  268. assert.Assert(t, err != nil)
  269. assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
  270. }
  271. func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
  272. defer setupTestV1(t)()
  273. d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  274. ctrl.reqRes.Allow = true
  275. ctrl.resRes.Allow = true
  276. client, err := d.NewClient()
  277. assert.NilError(t, err)
  278. _, err = client.ServerVersion(context.Background())
  279. assert.NilError(t, err)
  280. // assert plugin is only called once..
  281. assert.Equal(t, 1, ctrl.versionReqCount)
  282. assert.Equal(t, 1, ctrl.versionResCount)
  283. }
  284. func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
  285. defer setupTestV1(t)()
  286. ctrl.reqRes.Allow = true
  287. ctrl.resRes.Allow = true
  288. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  289. client, err := d.NewClient()
  290. assert.NilError(t, err)
  291. ctx := context.Background()
  292. tmp, err := ioutil.TempDir("", "test-authz-load-import")
  293. assert.NilError(t, err)
  294. defer os.RemoveAll(tmp)
  295. savedImagePath := filepath.Join(tmp, "save.tar")
  296. err = imageSave(client, savedImagePath, "busybox")
  297. assert.NilError(t, err)
  298. err = imageLoad(client, savedImagePath)
  299. assert.NilError(t, err)
  300. exportedImagePath := filepath.Join(tmp, "export.tar")
  301. cID := container.Run(t, ctx, client)
  302. responseReader, err := client.ContainerExport(context.Background(), cID)
  303. assert.NilError(t, err)
  304. defer responseReader.Close()
  305. file, err := os.Create(exportedImagePath)
  306. assert.NilError(t, err)
  307. defer file.Close()
  308. _, err = io.Copy(file, responseReader)
  309. assert.NilError(t, err)
  310. err = imageImport(client, exportedImagePath)
  311. assert.NilError(t, err)
  312. }
  313. func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
  314. defer setupTestV1(t)()
  315. ctrl.reqRes.Allow = true
  316. ctrl.resRes.Allow = true
  317. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  318. dir, err := ioutil.TempDir("", t.Name())
  319. assert.Assert(t, err)
  320. defer os.RemoveAll(dir)
  321. f, err := ioutil.TempFile(dir, "send")
  322. assert.Assert(t, err)
  323. defer f.Close()
  324. buf := make([]byte, 1024)
  325. fileSize := len(buf) * 1024 * 10
  326. for written := 0; written < fileSize; {
  327. n, err := f.Write(buf)
  328. assert.Assert(t, err)
  329. written += n
  330. }
  331. ctx := context.Background()
  332. client, err := d.NewClient()
  333. assert.Assert(t, err)
  334. cID := container.Run(t, ctx, client)
  335. defer client.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
  336. _, err = f.Seek(0, io.SeekStart)
  337. assert.Assert(t, err)
  338. srcInfo, err := archive.CopyInfoSourcePath(f.Name(), false)
  339. assert.Assert(t, err)
  340. srcArchive, err := archive.TarResource(srcInfo)
  341. assert.Assert(t, err)
  342. defer srcArchive.Close()
  343. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
  344. assert.Assert(t, err)
  345. err = client.CopyToContainer(ctx, cID, dstDir, preparedArchive, types.CopyToContainerOptions{})
  346. assert.Assert(t, err)
  347. rdr, _, err := client.CopyFromContainer(ctx, cID, "/test")
  348. assert.Assert(t, err)
  349. _, err = io.Copy(ioutil.Discard, rdr)
  350. assert.Assert(t, err)
  351. }
  352. func imageSave(client client.APIClient, path, image string) error {
  353. ctx := context.Background()
  354. responseReader, err := client.ImageSave(ctx, []string{image})
  355. if err != nil {
  356. return err
  357. }
  358. defer responseReader.Close()
  359. file, err := os.Create(path)
  360. if err != nil {
  361. return err
  362. }
  363. defer file.Close()
  364. _, err = io.Copy(file, responseReader)
  365. return err
  366. }
  367. func imageLoad(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. quiet := true
  374. ctx := context.Background()
  375. response, err := client.ImageLoad(ctx, file, quiet)
  376. if err != nil {
  377. return err
  378. }
  379. defer response.Body.Close()
  380. return nil
  381. }
  382. func imageImport(client client.APIClient, path string) error {
  383. file, err := os.Open(path)
  384. if err != nil {
  385. return err
  386. }
  387. defer file.Close()
  388. options := types.ImageImportOptions{}
  389. ref := ""
  390. source := types.ImageImportSource{
  391. Source: file,
  392. SourceName: "-",
  393. }
  394. ctx := context.Background()
  395. responseReader, err := client.ImageImport(ctx, source, ref, options)
  396. if err != nil {
  397. return err
  398. }
  399. defer responseReader.Close()
  400. return nil
  401. }
  402. func TestAuthZPluginHeader(t *testing.T) {
  403. defer setupTestV1(t)()
  404. ctrl.reqRes.Allow = true
  405. ctrl.resRes.Allow = true
  406. d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin)
  407. daemonURL, err := url.Parse(d.Sock())
  408. assert.NilError(t, err)
  409. conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
  410. assert.NilError(t, err)
  411. client := httputil.NewClientConn(conn, nil)
  412. req, err := http.NewRequest("GET", "/version", nil)
  413. assert.NilError(t, err)
  414. resp, err := client.Do(req)
  415. assert.NilError(t, err)
  416. assert.Equal(t, "application/json", resp.Header["Content-Type"][0])
  417. }
  418. // assertURIRecorded verifies that the given URI was sent and recorded
  419. // in the authz plugin
  420. func assertURIRecorded(t *testing.T, uris []string, uri string) {
  421. var found bool
  422. for _, u := range uris {
  423. if strings.Contains(u, uri) {
  424. found = true
  425. break
  426. }
  427. }
  428. if !found {
  429. t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
  430. }
  431. }