authz_plugin_test.go 14 KB

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