authz_plugin_test.go 14 KB

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