authz_plugin_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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. "github.com/docker/docker/api/types/container"
  20. eventtypes "github.com/docker/docker/api/types/events"
  21. networktypes "github.com/docker/docker/api/types/network"
  22. "github.com/docker/docker/client"
  23. "github.com/docker/docker/integration/internal/request"
  24. "github.com/docker/docker/internal/test/environment"
  25. "github.com/docker/docker/pkg/authorization"
  26. "github.com/gotestyourself/gotestyourself/skip"
  27. "github.com/stretchr/testify/require"
  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. require.Nil(t, err)
  53. fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
  54. err = ioutil.WriteFile(fileName, []byte(server.URL), 0644)
  55. require.Nil(t, err)
  56. return func() {
  57. err := os.RemoveAll("/etc/docker/plugins")
  58. require.Nil(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. client, err := d.NewClient()
  78. require.Nil(t, err)
  79. // Ensure command successful
  80. createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{"top"}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "")
  81. require.Nil(t, err)
  82. err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{})
  83. require.Nil(t, err)
  84. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  85. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", createResponse.ID))
  86. _, err = client.ServerVersion(context.Background())
  87. require.Nil(t, err)
  88. require.Equal(t, 1, ctrl.versionReqCount)
  89. require.Equal(t, 1, ctrl.versionResCount)
  90. }
  91. func TestAuthZPluginTLS(t *testing.T) {
  92. defer setupTestV1(t)()
  93. const (
  94. testDaemonHTTPSAddr = "tcp://localhost:4271"
  95. cacertPath = "../../testdata/https/ca.pem"
  96. serverCertPath = "../../testdata/https/server-cert.pem"
  97. serverKeyPath = "../../testdata/https/server-key.pem"
  98. clientCertPath = "../../testdata/https/client-cert.pem"
  99. clientKeyPath = "../../testdata/https/client-key.pem"
  100. )
  101. d.Start(t,
  102. "--authorization-plugin="+testAuthZPlugin,
  103. "--tlsverify",
  104. "--tlscacert", cacertPath,
  105. "--tlscert", serverCertPath,
  106. "--tlskey", serverKeyPath,
  107. "-H", testDaemonHTTPSAddr)
  108. ctrl.reqRes.Allow = true
  109. ctrl.resRes.Allow = true
  110. client, err := request.NewTLSAPIClient(t, testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
  111. require.Nil(t, err)
  112. _, err = client.ServerVersion(context.Background())
  113. require.Nil(t, err)
  114. require.Equal(t, "client", ctrl.reqUser)
  115. require.Equal(t, "client", ctrl.resUser)
  116. }
  117. func TestAuthZPluginDenyRequest(t *testing.T) {
  118. defer setupTestV1(t)()
  119. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  120. ctrl.reqRes.Allow = false
  121. ctrl.reqRes.Msg = unauthorizedMessage
  122. client, err := d.NewClient()
  123. require.Nil(t, err)
  124. // Ensure command is blocked
  125. _, err = client.ServerVersion(context.Background())
  126. require.NotNil(t, err)
  127. require.Equal(t, 1, ctrl.versionReqCount)
  128. require.Equal(t, 0, ctrl.versionResCount)
  129. // Ensure unauthorized message appears in response
  130. require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  131. }
  132. // TestAuthZPluginAPIDenyResponse validates that when authorization
  133. // plugin deny the request, the status code is forbidden
  134. func TestAuthZPluginAPIDenyResponse(t *testing.T) {
  135. defer setupTestV1(t)()
  136. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  137. ctrl.reqRes.Allow = false
  138. ctrl.resRes.Msg = unauthorizedMessage
  139. daemonURL, err := url.Parse(d.Sock())
  140. require.Nil(t, err)
  141. conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
  142. require.Nil(t, err)
  143. client := httputil.NewClientConn(conn, nil)
  144. req, err := http.NewRequest("GET", "/version", nil)
  145. require.Nil(t, err)
  146. resp, err := client.Do(req)
  147. require.Nil(t, err)
  148. require.Equal(t, http.StatusForbidden, resp.StatusCode)
  149. }
  150. func TestAuthZPluginDenyResponse(t *testing.T) {
  151. defer setupTestV1(t)()
  152. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  153. ctrl.reqRes.Allow = true
  154. ctrl.resRes.Allow = false
  155. ctrl.resRes.Msg = unauthorizedMessage
  156. client, err := d.NewClient()
  157. require.Nil(t, err)
  158. // Ensure command is blocked
  159. _, err = client.ServerVersion(context.Background())
  160. require.NotNil(t, err)
  161. require.Equal(t, 1, ctrl.versionReqCount)
  162. require.Equal(t, 1, ctrl.versionResCount)
  163. // Ensure unauthorized message appears in response
  164. require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
  165. }
  166. // TestAuthZPluginAllowEventStream verifies event stream propagates
  167. // correctly after request pass through by the authorization plugin
  168. func TestAuthZPluginAllowEventStream(t *testing.T) {
  169. skip.IfCondition(t, testEnv.DaemonInfo.OSType != "linux")
  170. defer setupTestV1(t)()
  171. ctrl.reqRes.Allow = true
  172. ctrl.resRes.Allow = true
  173. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin)
  174. client, err := d.NewClient()
  175. require.Nil(t, err)
  176. startTime := strconv.FormatInt(systemTime(t, client, testEnv).Unix(), 10)
  177. events, errs, cancel := systemEventsSince(client, startTime)
  178. defer cancel()
  179. // Create a container and wait for the creation events
  180. createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{"top"}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "")
  181. require.Nil(t, err)
  182. err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{})
  183. require.Nil(t, err)
  184. for i := 0; i < 100; i++ {
  185. c, err := client.ContainerInspect(context.Background(), createResponse.ID)
  186. require.Nil(t, err)
  187. if c.State.Running {
  188. break
  189. }
  190. if i == 99 {
  191. t.Fatal("Container didn't run within 10s")
  192. }
  193. time.Sleep(100 * time.Millisecond)
  194. }
  195. created := false
  196. started := false
  197. for !created && !started {
  198. select {
  199. case event := <-events:
  200. if event.Type == eventtypes.ContainerEventType && event.Actor.ID == createResponse.ID {
  201. if event.Action == "create" {
  202. created = true
  203. }
  204. if event.Action == "start" {
  205. started = true
  206. }
  207. }
  208. case err := <-errs:
  209. if err == io.EOF {
  210. t.Fatal("premature end of event stream")
  211. }
  212. require.Nil(t, err)
  213. case <-time.After(30 * time.Second):
  214. // Fail the test
  215. t.Fatal("event stream timeout")
  216. }
  217. }
  218. // Ensure both events and container endpoints are passed to the
  219. // authorization plugin
  220. assertURIRecorded(t, ctrl.requestsURIs, "/events")
  221. assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
  222. assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", createResponse.ID))
  223. }
  224. func systemTime(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
  225. if testEnv.IsLocalDaemon() {
  226. return time.Now()
  227. }
  228. ctx := context.Background()
  229. info, err := client.Info(ctx)
  230. require.Nil(t, err)
  231. dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
  232. require.Nil(t, err, "invalid time format in GET /info response")
  233. return dt
  234. }
  235. func systemEventsSince(client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) {
  236. eventOptions := types.EventsOptions{
  237. Since: since,
  238. }
  239. ctx, cancel := context.WithCancel(context.Background())
  240. events, errs := client.Events(ctx, eventOptions)
  241. return events, errs, cancel
  242. }
  243. func TestAuthZPluginErrorResponse(t *testing.T) {
  244. defer setupTestV1(t)()
  245. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  246. ctrl.reqRes.Allow = true
  247. ctrl.resRes.Err = errorMessage
  248. client, err := d.NewClient()
  249. require.Nil(t, err)
  250. // Ensure command is blocked
  251. _, err = client.ServerVersion(context.Background())
  252. require.NotNil(t, err)
  253. require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
  254. }
  255. func TestAuthZPluginErrorRequest(t *testing.T) {
  256. defer setupTestV1(t)()
  257. d.Start(t, "--authorization-plugin="+testAuthZPlugin)
  258. ctrl.reqRes.Err = errorMessage
  259. client, err := d.NewClient()
  260. require.Nil(t, err)
  261. // Ensure command is blocked
  262. _, err = client.ServerVersion(context.Background())
  263. require.NotNil(t, err)
  264. require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
  265. }
  266. func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
  267. defer setupTestV1(t)()
  268. d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  269. ctrl.reqRes.Allow = true
  270. ctrl.resRes.Allow = true
  271. client, err := d.NewClient()
  272. require.Nil(t, err)
  273. _, err = client.ServerVersion(context.Background())
  274. require.Nil(t, err)
  275. // assert plugin is only called once..
  276. require.Equal(t, 1, ctrl.versionReqCount)
  277. require.Equal(t, 1, ctrl.versionResCount)
  278. }
  279. func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
  280. defer setupTestV1(t)()
  281. ctrl.reqRes.Allow = true
  282. ctrl.resRes.Allow = true
  283. d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
  284. client, err := d.NewClient()
  285. require.Nil(t, err)
  286. tmp, err := ioutil.TempDir("", "test-authz-load-import")
  287. require.Nil(t, err)
  288. defer os.RemoveAll(tmp)
  289. savedImagePath := filepath.Join(tmp, "save.tar")
  290. err = imageSave(client, savedImagePath, "busybox")
  291. require.Nil(t, err)
  292. err = imageLoad(client, savedImagePath)
  293. require.Nil(t, err)
  294. exportedImagePath := filepath.Join(tmp, "export.tar")
  295. createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "")
  296. require.Nil(t, err)
  297. err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{})
  298. require.Nil(t, err)
  299. responseReader, err := client.ContainerExport(context.Background(), createResponse.ID)
  300. require.Nil(t, err)
  301. defer responseReader.Close()
  302. file, err := os.Create(exportedImagePath)
  303. require.Nil(t, err)
  304. defer file.Close()
  305. _, err = io.Copy(file, responseReader)
  306. require.Nil(t, err)
  307. err = imageImport(client, exportedImagePath)
  308. require.Nil(t, err)
  309. }
  310. func imageSave(client client.APIClient, path, image string) error {
  311. ctx := context.Background()
  312. responseReader, err := client.ImageSave(ctx, []string{image})
  313. if err != nil {
  314. return err
  315. }
  316. defer responseReader.Close()
  317. file, err := os.Create(path)
  318. if err != nil {
  319. return err
  320. }
  321. defer file.Close()
  322. _, err = io.Copy(file, responseReader)
  323. return err
  324. }
  325. func imageLoad(client client.APIClient, path string) error {
  326. file, err := os.Open(path)
  327. if err != nil {
  328. return err
  329. }
  330. defer file.Close()
  331. quiet := true
  332. ctx := context.Background()
  333. response, err := client.ImageLoad(ctx, file, quiet)
  334. if err != nil {
  335. return err
  336. }
  337. defer response.Body.Close()
  338. return nil
  339. }
  340. func imageImport(client client.APIClient, path string) error {
  341. file, err := os.Open(path)
  342. if err != nil {
  343. return err
  344. }
  345. defer file.Close()
  346. options := types.ImageImportOptions{}
  347. ref := ""
  348. source := types.ImageImportSource{
  349. Source: file,
  350. SourceName: "-",
  351. }
  352. ctx := context.Background()
  353. responseReader, err := client.ImageImport(ctx, source, ref, options)
  354. if err != nil {
  355. return err
  356. }
  357. defer responseReader.Close()
  358. return nil
  359. }
  360. func TestAuthZPluginHeader(t *testing.T) {
  361. defer setupTestV1(t)()
  362. ctrl.reqRes.Allow = true
  363. ctrl.resRes.Allow = true
  364. d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin)
  365. daemonURL, err := url.Parse(d.Sock())
  366. require.Nil(t, err)
  367. conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
  368. require.Nil(t, err)
  369. client := httputil.NewClientConn(conn, nil)
  370. req, err := http.NewRequest("GET", "/version", nil)
  371. require.Nil(t, err)
  372. resp, err := client.Do(req)
  373. require.Nil(t, err)
  374. require.Equal(t, "application/json", resp.Header["Content-Type"][0])
  375. }
  376. // assertURIRecorded verifies that the given URI was sent and recorded
  377. // in the authz plugin
  378. func assertURIRecorded(t *testing.T, uris []string, uri string) {
  379. var found bool
  380. for _, u := range uris {
  381. if strings.Contains(u, uri) {
  382. found = true
  383. break
  384. }
  385. }
  386. if !found {
  387. t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
  388. }
  389. }