diff --git a/hack/make/.integration-test-helpers b/hack/make/.integration-test-helpers index e3cb7d84a7..d9b4cf15b5 100644 --- a/hack/make/.integration-test-helpers +++ b/hack/make/.integration-test-helpers @@ -15,7 +15,7 @@ source "$SCRIPTDIR/make/.go-autogen" integration_api_dirs=${TEST_INTEGRATION_DIR:-"$( find ./integration -type d | - grep -vE '^(./integration$|./integration/util)')"} + grep -vE '^(./integration($|/util|/internal|/testdata|/plugin$))')"} run_test_integration() { [[ "$TESTFLAGS" != *-check.f* ]] && run_test_integration_suites diff --git a/integration-cli/docker_cli_authz_plugin_v2_test.go b/integration-cli/docker_cli_authz_plugin_v2_test.go deleted file mode 100644 index 30026f7fcd..0000000000 --- a/integration-cli/docker_cli_authz_plugin_v2_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// +build !windows - -package main - -import ( - "fmt" - - "github.com/docker/docker/integration-cli/checker" - "github.com/docker/docker/integration-cli/daemon" - "github.com/go-check/check" -) - -var ( - authzPluginName = "riyaz/authz-no-volume-plugin" - authzPluginTag = "latest" - authzPluginNameWithTag = authzPluginName + ":" + authzPluginTag - authzPluginBadManifestName = "riyaz/authz-plugin-bad-manifest" - nonexistentAuthzPluginName = "riyaz/nonexistent-authz-plugin" -) - -func init() { - check.Suite(&DockerAuthzV2Suite{ - ds: &DockerSuite{}, - }) -} - -type DockerAuthzV2Suite struct { - ds *DockerSuite - d *daemon.Daemon -} - -func (s *DockerAuthzV2Suite) SetUpTest(c *check.C) { - testRequires(c, DaemonIsLinux, Network, SameHostDaemon) - s.d = daemon.New(c, dockerBinary, dockerdBinary, daemon.Config{ - Experimental: testEnv.ExperimentalDaemon(), - }) - s.d.Start(c) -} - -func (s *DockerAuthzV2Suite) TearDownTest(c *check.C) { - if s.d != nil { - s.d.Stop(c) - s.ds.TearDownTest(c) - } -} - -func (s *DockerAuthzV2Suite) TestAuthZPluginAllowNonVolumeRequest(c *check.C) { - testRequires(c, DaemonIsLinux, IsAmd64, Network) - - // Install authz plugin - _, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - // start the daemon with the plugin and load busybox, --net=none build fails otherwise - // because it needs to pull busybox - s.d.Restart(c, "--authorization-plugin="+authzPluginNameWithTag) - s.d.LoadBusybox(c) - - // defer disabling the plugin - defer func() { - s.d.Restart(c) - _, err = s.d.Cmd("plugin", "disable", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - _, err = s.d.Cmd("plugin", "rm", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - }() - - // Ensure docker run command and accompanying docker ps are successful - _, err = s.d.Cmd("run", "-d", "busybox", "top") - c.Assert(err, check.IsNil) -} - -func (s *DockerAuthzV2Suite) TestAuthZPluginDisable(c *check.C) { - testRequires(c, DaemonIsLinux, IsAmd64, Network) - // Install authz plugin - _, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - // start the daemon with the plugin and load busybox, --net=none build fails otherwise - // because it needs to pull busybox - s.d.Restart(c, "--authorization-plugin="+authzPluginNameWithTag) - s.d.LoadBusybox(c) - - // defer removing the plugin - defer func() { - s.d.Restart(c) - _, err = s.d.Cmd("plugin", "rm", "-f", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - }() - - out, err := s.d.Cmd("volume", "create") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) - - // disable the plugin - _, err = s.d.Cmd("plugin", "disable", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - - // now test to see if the docker api works. - _, err = s.d.Cmd("volume", "create") - c.Assert(err, checker.IsNil) -} - -func (s *DockerAuthzV2Suite) TestAuthZPluginRejectVolumeRequests(c *check.C) { - testRequires(c, DaemonIsLinux, IsAmd64, Network) - // Install authz plugin - _, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - - // restart the daemon with the plugin - s.d.Restart(c, "--authorization-plugin="+authzPluginNameWithTag) - - // defer disabling the plugin - defer func() { - s.d.Restart(c) - _, err = s.d.Cmd("plugin", "disable", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - _, err = s.d.Cmd("plugin", "rm", authzPluginNameWithTag) - c.Assert(err, checker.IsNil) - }() - - out, err := s.d.Cmd("volume", "create") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) - - out, err = s.d.Cmd("volume", "ls") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) - - // The plugin will block the command before it can determine the volume does not exist - out, err = s.d.Cmd("volume", "rm", "test") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) - - out, err = s.d.Cmd("volume", "inspect", "test") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) - - out, err = s.d.Cmd("volume", "prune", "-f") - c.Assert(err, check.NotNil) - c.Assert(out, checker.Contains, fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag)) -} - -func (s *DockerAuthzV2Suite) TestAuthZPluginBadManifestFailsDaemonStart(c *check.C) { - testRequires(c, DaemonIsLinux, IsAmd64, Network) - // Install authz plugin with bad manifest - _, err := s.d.Cmd("plugin", "install", "--grant-all-permissions", authzPluginBadManifestName) - c.Assert(err, checker.IsNil) - - // start the daemon with the plugin, it will error - c.Assert(s.d.RestartWithError("--authorization-plugin="+authzPluginBadManifestName), check.NotNil) - - // restarting the daemon without requiring the plugin will succeed - s.d.Restart(c) -} - -func (s *DockerAuthzV2Suite) TestNonexistentAuthZPluginFailsDaemonStart(c *check.C) { - testRequires(c, DaemonIsLinux, Network) - // start the daemon with a non-existent authz plugin, it will error - c.Assert(s.d.RestartWithError("--authorization-plugin="+nonexistentAuthzPluginName), check.NotNil) - - // restarting the daemon without requiring the plugin will succeed - s.d.Start(c) -} diff --git a/integration-cli/docker_cli_authz_unix_test.go b/integration-cli/docker_cli_authz_unix_test.go deleted file mode 100644 index 8a1bd023ea..0000000000 --- a/integration-cli/docker_cli_authz_unix_test.go +++ /dev/null @@ -1,470 +0,0 @@ -// +build !windows - -package main - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - - "bufio" - "bytes" - "os/exec" - "strconv" - "time" - - "net" - "net/http/httputil" - "net/url" - - "github.com/docker/docker/integration-cli/checker" - "github.com/docker/docker/integration-cli/daemon" - "github.com/docker/docker/pkg/authorization" - "github.com/docker/docker/pkg/plugins" - "github.com/go-check/check" -) - -const ( - testAuthZPlugin = "authzplugin" - unauthorizedMessage = "User unauthorized authz plugin" - errorMessage = "something went wrong..." - containerListAPI = "/containers/json" -) - -var ( - alwaysAllowed = []string{"/_ping", "/info"} -) - -func init() { - check.Suite(&DockerAuthzSuite{ - ds: &DockerSuite{}, - }) -} - -type DockerAuthzSuite struct { - server *httptest.Server - ds *DockerSuite - d *daemon.Daemon - ctrl *authorizationController -} - -type authorizationController struct { - reqRes authorization.Response // reqRes holds the plugin response to the initial client request - resRes authorization.Response // resRes holds the plugin response to the daemon response - psRequestCnt int // psRequestCnt counts the number of calls to list container request api - psResponseCnt int // psResponseCnt counts the number of calls to list containers response API - requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller - reqUser string - resUser string -} - -func (s *DockerAuthzSuite) SetUpTest(c *check.C) { - testRequires(c, SameHostDaemon) - s.d = daemon.New(c, dockerBinary, dockerdBinary, daemon.Config{ - Experimental: testEnv.ExperimentalDaemon(), - }) - s.ctrl = &authorizationController{} -} - -func (s *DockerAuthzSuite) TearDownTest(c *check.C) { - if s.d != nil { - s.d.Stop(c) - s.ds.TearDownTest(c) - s.ctrl = nil - } -} - -func (s *DockerAuthzSuite) SetUpSuite(c *check.C) { - mux := http.NewServeMux() - s.server = httptest.NewServer(mux) - - mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { - b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}}) - c.Assert(err, check.IsNil) - w.Write(b) - }) - - mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - c.Assert(err, check.IsNil) - authReq := authorization.Request{} - err = json.Unmarshal(body, &authReq) - c.Assert(err, check.IsNil) - - assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody) - assertAuthHeaders(c, authReq.RequestHeaders) - - // Count only container list api - if strings.HasSuffix(authReq.RequestURI, containerListAPI) { - s.ctrl.psRequestCnt++ - } - - s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI) - - reqRes := s.ctrl.reqRes - if isAllowed(authReq.RequestURI) { - reqRes = authorization.Response{Allow: true} - } - if reqRes.Err != "" { - w.WriteHeader(http.StatusInternalServerError) - } - b, err := json.Marshal(reqRes) - c.Assert(err, check.IsNil) - s.ctrl.reqUser = authReq.User - w.Write(b) - }) - - mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - c.Assert(err, check.IsNil) - authReq := authorization.Request{} - err = json.Unmarshal(body, &authReq) - c.Assert(err, check.IsNil) - - assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody) - assertAuthHeaders(c, authReq.ResponseHeaders) - - // Count only container list api - if strings.HasSuffix(authReq.RequestURI, containerListAPI) { - s.ctrl.psResponseCnt++ - } - resRes := s.ctrl.resRes - if isAllowed(authReq.RequestURI) { - resRes = authorization.Response{Allow: true} - } - if resRes.Err != "" { - w.WriteHeader(http.StatusInternalServerError) - } - b, err := json.Marshal(resRes) - c.Assert(err, check.IsNil) - s.ctrl.resUser = authReq.User - w.Write(b) - }) - - err := os.MkdirAll("/etc/docker/plugins", 0755) - c.Assert(err, checker.IsNil) - - fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin) - err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644) - c.Assert(err, checker.IsNil) -} - -// check for always allowed endpoints to not inhibit test framework functions -func isAllowed(reqURI string) bool { - for _, endpoint := range alwaysAllowed { - if strings.HasSuffix(reqURI, endpoint) { - return true - } - } - return false -} - -// assertAuthHeaders validates authentication headers are removed -func assertAuthHeaders(c *check.C, headers map[string]string) error { - for k := range headers { - if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") { - c.Errorf("Found authentication headers in request '%v'", headers) - } - } - return nil -} - -// assertBody asserts that body is removed for non text/json requests -func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) { - if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 { - //return fmt.Errorf("Body included for authentication endpoint %s", string(body)) - c.Errorf("Body included for authentication endpoint %s", string(body)) - } - - for k, v := range headers { - if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" { - return - } - } - if len(body) > 0 { - c.Errorf("Body included while it should not (Headers: '%v')", headers) - } -} - -func (s *DockerAuthzSuite) TearDownSuite(c *check.C) { - if s.server == nil { - return - } - - s.server.Close() - - err := os.RemoveAll("/etc/docker/plugins") - c.Assert(err, checker.IsNil) -} - -func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) { - // start the daemon and load busybox, --net=none build fails otherwise - // cause it needs to pull busybox - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - s.d.LoadBusybox(c) - - // Ensure command successful - out, err := s.d.Cmd("run", "-d", "busybox", "top") - c.Assert(err, check.IsNil) - - id := strings.TrimSpace(out) - assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create") - assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id)) -} - -func (s *DockerAuthzSuite) TestAuthZPluginTls(c *check.C) { - - const testDaemonHTTPSAddr = "tcp://localhost:4271" - // start the daemon and load busybox, --net=none build fails otherwise - // cause it needs to pull busybox - s.d.Start(c, - "--authorization-plugin="+testAuthZPlugin, - "--tlsverify", - "--tlscacert", - "fixtures/https/ca.pem", - "--tlscert", - "fixtures/https/server-cert.pem", - "--tlskey", - "fixtures/https/server-key.pem", - "-H", testDaemonHTTPSAddr) - - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - - out, _ := dockerCmd( - c, - "--tlsverify", - "--tlscacert", "fixtures/https/ca.pem", - "--tlscert", "fixtures/https/client-cert.pem", - "--tlskey", "fixtures/https/client-key.pem", - "-H", - testDaemonHTTPSAddr, - "version", - ) - if !strings.Contains(out, "Server") { - c.Fatalf("docker version should return information of server side") - } - - c.Assert(s.ctrl.reqUser, check.Equals, "client") - c.Assert(s.ctrl.resUser, check.Equals, "client") -} - -func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = false - s.ctrl.reqRes.Msg = unauthorizedMessage - - // Ensure command is blocked - res, err := s.d.Cmd("ps") - c.Assert(err, check.NotNil) - c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) - c.Assert(s.ctrl.psResponseCnt, check.Equals, 0) - - // Ensure unauthorized message appears in response - c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage)) -} - -// TestAuthZPluginAPIDenyResponse validates that when authorization plugin deny the request, the status code is forbidden -func (s *DockerAuthzSuite) TestAuthZPluginAPIDenyResponse(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = false - s.ctrl.resRes.Msg = unauthorizedMessage - - daemonURL, err := url.Parse(s.d.Sock()) - - conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) - c.Assert(err, check.IsNil) - client := httputil.NewClientConn(conn, nil) - req, err := http.NewRequest("GET", "/version", nil) - c.Assert(err, check.IsNil) - resp, err := client.Do(req) - - c.Assert(err, check.IsNil) - c.Assert(resp.StatusCode, checker.Equals, http.StatusForbidden) - c.Assert(err, checker.IsNil) -} - -func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = false - s.ctrl.resRes.Msg = unauthorizedMessage - - // Ensure command is blocked - res, err := s.d.Cmd("ps") - c.Assert(err, check.NotNil) - c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) - c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) - - // Ensure unauthorized message appears in response - c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage)) -} - -// TestAuthZPluginAllowEventStream verifies event stream propagates correctly after request pass through by the authorization plugin -func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) { - testRequires(c, DaemonIsLinux) - - // start the daemon and load busybox to avoid pulling busybox from Docker Hub - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - s.d.LoadBusybox(c) - - startTime := strconv.FormatInt(daemonTime(c).Unix(), 10) - // Add another command to to enable event pipelining - eventsCmd := exec.Command(dockerBinary, "--host", s.d.Sock(), "events", "--since", startTime) - stdout, err := eventsCmd.StdoutPipe() - if err != nil { - c.Assert(err, check.IsNil) - } - - observer := eventObserver{ - buffer: new(bytes.Buffer), - command: eventsCmd, - scanner: bufio.NewScanner(stdout), - startTime: startTime, - } - - err = observer.Start() - c.Assert(err, checker.IsNil) - defer observer.Stop() - - // Create a container and wait for the creation events - out, err := s.d.Cmd("run", "-d", "busybox", "top") - c.Assert(err, check.IsNil, check.Commentf(out)) - containerID := strings.TrimSpace(out) - c.Assert(s.d.WaitRun(containerID), checker.IsNil) - - events := map[string]chan bool{ - "create": make(chan bool, 1), - "start": make(chan bool, 1), - } - - matcher := matchEventLine(containerID, "container", events) - processor := processEventMatch(events) - go observer.Match(matcher, processor) - - // Ensure all events are received - for event, eventChannel := range events { - - select { - case <-time.After(30 * time.Second): - // Fail the test - observer.CheckEventError(c, containerID, event, matcher) - c.FailNow() - case <-eventChannel: - // Ignore, event received - } - } - - // Ensure both events and container endpoints are passed to the authorization plugin - assertURIRecorded(c, s.ctrl.requestsURIs, "/events") - assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create") - assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", containerID)) -} - -func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Err = errorMessage - - // Ensure command is blocked - res, err := s.d.Cmd("ps") - c.Assert(err, check.NotNil) - - c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage)) -} - -func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Err = errorMessage - - // Ensure command is blocked - res, err := s.d.Cmd("ps") - c.Assert(err, check.NotNil) - - c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage)) -} - -func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) - - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - - out, err := s.d.Cmd("ps") - c.Assert(err, check.IsNil, check.Commentf(out)) - - // assert plugin is only called once.. - c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) - c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) -} - -func (s *DockerAuthzSuite) TestAuthZPluginEnsureLoadImportWorking(c *check.C) { - s.d.Start(c, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - s.d.LoadBusybox(c) - - tmp, err := ioutil.TempDir("", "test-authz-load-import") - c.Assert(err, check.IsNil) - defer os.RemoveAll(tmp) - - savedImagePath := filepath.Join(tmp, "save.tar") - - out, err := s.d.Cmd("save", "-o", savedImagePath, "busybox") - c.Assert(err, check.IsNil, check.Commentf(out)) - out, err = s.d.Cmd("load", "--input", savedImagePath) - c.Assert(err, check.IsNil, check.Commentf(out)) - - exportedImagePath := filepath.Join(tmp, "export.tar") - - out, err = s.d.Cmd("run", "-d", "--name", "testexport", "busybox") - c.Assert(err, check.IsNil, check.Commentf(out)) - out, err = s.d.Cmd("export", "-o", exportedImagePath, "testexport") - c.Assert(err, check.IsNil, check.Commentf(out)) - out, err = s.d.Cmd("import", exportedImagePath) - c.Assert(err, check.IsNil, check.Commentf(out)) -} - -func (s *DockerAuthzSuite) TestAuthZPluginHeader(c *check.C) { - s.d.Start(c, "--debug", "--authorization-plugin="+testAuthZPlugin) - s.ctrl.reqRes.Allow = true - s.ctrl.resRes.Allow = true - s.d.LoadBusybox(c) - - daemonURL, err := url.Parse(s.d.Sock()) - - conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) - c.Assert(err, check.IsNil) - client := httputil.NewClientConn(conn, nil) - req, err := http.NewRequest("GET", "/version", nil) - c.Assert(err, check.IsNil) - resp, err := client.Do(req) - - c.Assert(err, check.IsNil) - c.Assert(resp.Header["Content-Type"][0], checker.Equals, "application/json") -} - -// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin -func assertURIRecorded(c *check.C, uris []string, uri string) { - var found bool - for _, u := range uris { - if strings.Contains(u, uri) { - found = true - break - } - } - if !found { - c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ",")) - } -} diff --git a/integration-cli/fixtures/https/ca.pem b/integration-cli/fixtures/https/ca.pem deleted file mode 100644 index 6825d6d1bd..0000000000 --- a/integration-cli/fixtures/https/ca.pem +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID0TCCAzqgAwIBAgIJAP2r7GqEJwSnMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD -VQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMG -A1UEChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTERMA8GA1UEAxMI -Y2hhbmdlbWUxETAPBgNVBCkTCGNoYW5nZW1lMR8wHQYJKoZIhvcNAQkBFhBtYWls -QGhvc3QuZG9tYWluMB4XDTEzMTIwMzE2NTYzMFoXDTIzMTIwMTE2NTYzMFowgaIx -CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2Nv -MRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24xETAPBgNVBAsTCGNoYW5nZW1lMREwDwYD -VQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEW -EG1haWxAaG9zdC5kb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALAn -0xDw+5y7ZptQacq66pUhRu82JP2WU6IDgo5QUtNU6/CX5PwQATe/OnYTZQFbksxp -AU9boG0FCkgxfsgPYXEuZxVEGKI2fxfKHOZZI8mrkWmj6eWU/0cvCjGVc9rTITP5 -sNQvg+hORyVDdNp2IdsbMJayiB3AQYMFx3vSDOMTAgMBAAGjggELMIIBBzAdBgNV -HQ4EFgQUZu7DFz09q0QBa2+ymRm9qgK1NPswgdcGA1UdIwSBzzCBzIAUZu7DFz09 -q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJD -QTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24x -ETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMI -Y2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21haW6CCQD9q+xq -hCcEpzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAF8fJKKM+/oOdnNi -zEd0M1+PmZOyqvjYQn/2ZR8UHH6Imgc/OPQKZXf0bVE1Txc/DaUNn9Isd1SuCuaE -ic3vAIYYU7PmgeNN6vwec48V96T7jr+GAi6AVMhQEc2hHCfVtx11Xx+x6aHDZzJt -Zxtf5lL6KSO9Y+EFwM+rju6hm5hW ------END CERTIFICATE----- diff --git a/integration-cli/fixtures/https/ca.pem b/integration-cli/fixtures/https/ca.pem new file mode 120000 index 0000000000..70a3e6ce54 --- /dev/null +++ b/integration-cli/fixtures/https/ca.pem @@ -0,0 +1 @@ +../../../integration/testdata/https/ca.pem \ No newline at end of file diff --git a/integration-cli/fixtures/https/client-cert.pem b/integration-cli/fixtures/https/client-cert.pem deleted file mode 100644 index c05ed47c2c..0000000000 --- a/integration-cli/fixtures/https/client-cert.pem +++ /dev/null @@ -1,73 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 3 (0x3) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=changeme/name=changeme/emailAddress=mail@host.domain - Validity - Not Before: Dec 4 14:17:54 2013 GMT - Not After : Dec 2 14:17:54 2023 GMT - Subject: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=client/name=changeme/emailAddress=mail@host.domain - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (1024 bit) - Modulus: - 00:ca:c9:05:d0:09:4e:3e:a4:fc:d5:14:f4:a5:e8: - 34:d3:6b:51:e3:f3:62:ea:a1:f0:e8:ed:c4:2a:bc: - f0:4f:ca:07:df:e3:88:fa:f4:21:99:35:0e:3d:ea: - b0:86:e7:c4:d2:8a:83:2b:42:b8:ec:a3:99:62:70: - 81:46:cc:fc:a5:1d:d2:63:e8:eb:07:25:9a:e2:25: - 6d:11:56:f2:1a:51:a1:b6:3e:1c:57:32:e9:7b:2c: - aa:1b:cc:97:2d:89:2d:b1:c9:5e:35:28:4d:7c:fa: - 65:31:3e:f7:70:dd:6e:0b:3c:58:af:a8:2e:24:c0: - 7e:4e:78:7d:0a:9e:8f:42:43 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Comment: - Easy-RSA Generated Certificate - X509v3 Subject Key Identifier: - DE:42:EF:2D:98:A3:6C:A8:AA:E0:8C:71:2C:9D:64:23:A9:E2:7E:81 - X509v3 Authority Key Identifier: - keyid:66:EE:C3:17:3D:3D:AB:44:01:6B:6F:B2:99:19:BD:AA:02:B5:34:FB - DirName:/C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=changeme/CN=changeme/name=changeme/emailAddress=mail@host.domain - serial:FD:AB:EC:6A:84:27:04:A7 - - X509v3 Extended Key Usage: - TLS Web Client Authentication - X509v3 Key Usage: - Digital Signature - Signature Algorithm: sha1WithRSAEncryption - 1c:44:26:ea:e1:66:25:cb:e4:8e:57:1c:f6:b9:17:22:62:40: - 12:90:8f:3b:b2:61:7a:54:94:8f:b1:20:0b:bf:a3:51:e3:fa: - 1c:a1:be:92:3a:d0:76:44:c0:57:83:ab:6a:e4:1a:45:49:a4: - af:39:0d:60:32:fc:3a:be:d7:fb:5d:99:7a:1f:87:e7:d5:ab: - 84:a2:5e:90:d8:bf:fa:89:6d:32:26:02:5e:31:35:68:7f:31: - f5:6b:51:46:bc:af:70:ed:5a:09:7d:ec:b2:48:4f:fe:c5:2f: - 56:04:ad:f6:c1:d2:2a:e4:6a:c4:87:fe:08:35:c5:38:cb:5e: - 4a:c4 ------BEGIN CERTIFICATE----- -MIIEFTCCA36gAwIBAgIBAzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMCVVMx -CzAJBgNVBAgTAkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZv -cnQtRnVuc3RvbjERMA8GA1UECxMIY2hhbmdlbWUxETAPBgNVBAMTCGNoYW5nZW1l -MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0LmRv -bWFpbjAeFw0xMzEyMDQxNDE3NTRaFw0yMzEyMDIxNDE3NTRaMIGgMQswCQYDVQQG -EwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UE -ChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTEPMA0GA1UEAxMGY2xp -ZW50MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0 -LmRvbWFpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyskF0AlOPqT81RT0 -peg002tR4/Ni6qHw6O3EKrzwT8oH3+OI+vQhmTUOPeqwhufE0oqDK0K47KOZYnCB -Rsz8pR3SY+jrByWa4iVtEVbyGlGhtj4cVzLpeyyqG8yXLYktscleNShNfPplMT73 -cN1uCzxYr6guJMB+Tnh9Cp6PQkMCAwEAAaOCAVkwggFVMAkGA1UdEwQCMAAwLQYJ -YIZIAYb4QgENBCAWHkVhc3ktUlNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV -HQ4EFgQU3kLvLZijbKiq4IxxLJ1kI6nifoEwgdcGA1UdIwSBzzCBzIAUZu7DFz09 -q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJD -QTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24x -ETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMI -Y2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21haW6CCQD9q+xq -hCcEpzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcN -AQEFBQADgYEAHEQm6uFmJcvkjlcc9rkXImJAEpCPO7JhelSUj7EgC7+jUeP6HKG+ -kjrQdkTAV4OrauQaRUmkrzkNYDL8Or7X+12Zeh+H59WrhKJekNi/+oltMiYCXjE1 -aH8x9WtRRryvcO1aCX3sskhP/sUvVgSt9sHSKuRqxIf+CDXFOMteSsQ= ------END CERTIFICATE----- diff --git a/integration-cli/fixtures/https/client-cert.pem b/integration-cli/fixtures/https/client-cert.pem new file mode 120000 index 0000000000..458882026e --- /dev/null +++ b/integration-cli/fixtures/https/client-cert.pem @@ -0,0 +1 @@ +../../../integration/testdata/https/client-cert.pem \ No newline at end of file diff --git a/integration-cli/fixtures/https/client-key.pem b/integration-cli/fixtures/https/client-key.pem deleted file mode 100644 index b5c15f8dc7..0000000000 --- a/integration-cli/fixtures/https/client-key.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMrJBdAJTj6k/NUU -9KXoNNNrUePzYuqh8OjtxCq88E/KB9/jiPr0IZk1Dj3qsIbnxNKKgytCuOyjmWJw -gUbM/KUd0mPo6wclmuIlbRFW8hpRobY+HFcy6XssqhvMly2JLbHJXjUoTXz6ZTE+ -93Ddbgs8WK+oLiTAfk54fQqej0JDAgMBAAECgYBOFEzKp2qbMEexe9ofL2N3rDDh -xkrl8OijpzkLA6i78BxMFn4dsnZlWUpciMrjhsYAExkiRRSS+QMMJimAq1jzQqc3 -FAQV2XGYwkd0cUn7iZGvfNnEPysjsfyYQM+m+sT0ATj4BZjVShC6kkSjTdm1leLN -OSvcHdcu3Xxg9ufF0QJBAPYdnNt5sIndt2WECePuRVi+uF4mlxTobFY0fjn26yhC -4RsnhhD3Vldygo9gvnkwrAZYaALGSPBewes2InxvjA8CQQDS7erKiNXpwoqz5XiU -SVEsIIVTdWzBjGbIqMOu/hUwM5FK4j6JTBks0aTGMyh0YV9L1EzM0X79J29JahCe -iQKNAkBKNMOGqTpBV0hko1sYDk96YobUXG5RL4L6uvkUIQ7mJMQam+AgXXL7Ctuy -v0iu4a38e8tgisiTMP7nHHtpaXihAkAOiN54/lzfMsykANgCP9scE1GcoqbP34Dl -qttxH4kOPT9xzY1JoLjLYdbc4YGUI3GRpBt2sajygNkmUey7P+2xAkBBsVCZFvTw -qHvOpPS2kX5ml5xoc/QAHK9N7kR+X7XFYx82RTVSqJEK4lPb+aEWn+CjiIewO4Q5 -ksDFuNxAzbhl ------END PRIVATE KEY----- diff --git a/integration-cli/fixtures/https/client-key.pem b/integration-cli/fixtures/https/client-key.pem new file mode 120000 index 0000000000..d5f6bbee57 --- /dev/null +++ b/integration-cli/fixtures/https/client-key.pem @@ -0,0 +1 @@ +../../../integration/testdata/https/client-key.pem \ No newline at end of file diff --git a/integration-cli/fixtures/https/server-cert.pem b/integration-cli/fixtures/https/server-cert.pem deleted file mode 100644 index 08abfd1a3b..0000000000 --- a/integration-cli/fixtures/https/server-cert.pem +++ /dev/null @@ -1,76 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 4 (0x4) - Signature Algorithm: sha1WithRSAEncryption - Issuer: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=changeme/name=changeme/emailAddress=mail@host.domain - Validity - Not Before: Dec 4 15:01:20 2013 GMT - Not After : Dec 2 15:01:20 2023 GMT - Subject: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=*/name=changeme/emailAddress=mail@host.domain - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (1024 bit) - Modulus: - 00:c1:ff:7d:30:6f:64:4a:b1:92:b1:71:d1:c1:74: - e2:1d:db:2d:11:24:e1:00:d4:00:ae:6f:c8:9e:ae: - 67:b3:4a:bd:f7:e6:9e:57:6d:19:4c:3c:23:94:2d: - 3d:d6:63:84:d8:fa:76:2b:38:12:c1:ed:20:9d:32: - e0:e8:c2:bf:9a:77:70:04:3f:7f:ca:8c:2c:82:d6: - 3d:25:5c:02:1a:4f:64:93:03:dd:9c:42:97:5e:09: - 49:af:f0:c2:e1:30:08:0e:21:46:95:d1:13:59:c0: - c8:76:be:94:0d:8b:43:67:21:33:b2:08:60:9d:76: - a8:05:32:1e:f9:95:09:14:75 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: - CA:FALSE - Netscape Cert Type: - SSL Server - Netscape Comment: - Easy-RSA Generated Server Certificate - X509v3 Subject Key Identifier: - 14:02:FD:FD:DD:13:38:E0:71:EA:D1:BE:C0:0E:89:1A:2D:B6:19:06 - X509v3 Authority Key Identifier: - keyid:66:EE:C3:17:3D:3D:AB:44:01:6B:6F:B2:99:19:BD:AA:02:B5:34:FB - DirName:/C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=changeme/CN=changeme/name=changeme/emailAddress=mail@host.domain - serial:FD:AB:EC:6A:84:27:04:A7 - - X509v3 Extended Key Usage: - TLS Web Server Authentication - X509v3 Key Usage: - Digital Signature, Key Encipherment - Signature Algorithm: sha1WithRSAEncryption - 40:0f:10:39:c4:b7:0f:0d:2f:bf:d2:16:cc:8e:d3:9a:fb:8b: - ce:4b:7b:0d:48:77:ce:f1:fe:d5:8f:ea:b1:71:ed:49:1d:9f: - 23:3a:16:d4:70:7c:c5:29:bf:e4:90:34:d0:f0:00:24:f4:e4: - df:2c:c3:83:01:66:61:c9:a8:ab:29:e7:98:6d:27:89:4a:76: - c9:2e:19:8e:fe:6e:d5:f8:99:11:0e:97:67:4b:34:e3:1e:e3: - 9f:35:00:a5:32:f9:b5:2c:f2:e0:c5:2e:cc:81:bd:18:dd:5c: - 12:c8:6b:fa:0c:17:74:30:55:f6:6e:20:9a:6c:1e:09:b4:0c: - 15:42 ------BEGIN CERTIFICATE----- -MIIEKjCCA5OgAwIBAgIBBDANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMCVVMx -CzAJBgNVBAgTAkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZv -cnQtRnVuc3RvbjERMA8GA1UECxMIY2hhbmdlbWUxETAPBgNVBAMTCGNoYW5nZW1l -MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0LmRv -bWFpbjAeFw0xMzEyMDQxNTAxMjBaFw0yMzEyMDIxNTAxMjBaMIGbMQswCQYDVQQG -EwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UE -ChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTEKMAgGA1UEAxQBKjER -MA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21h -aW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMH/fTBvZEqxkrFx0cF04h3b -LREk4QDUAK5vyJ6uZ7NKvffmnldtGUw8I5QtPdZjhNj6dis4EsHtIJ0y4OjCv5p3 -cAQ/f8qMLILWPSVcAhpPZJMD3ZxCl14JSa/wwuEwCA4hRpXRE1nAyHa+lA2LQ2ch -M7IIYJ12qAUyHvmVCRR1AgMBAAGjggFzMIIBbzAJBgNVHRMEAjAAMBEGCWCGSAGG -+EIBAQQEAwIGQDA0BglghkgBhvhCAQ0EJxYlRWFzeS1SU0EgR2VuZXJhdGVkIFNl -cnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUFAL9/d0TOOBx6tG+wA6JGi22GQYw -gdcGA1UdIwSBzzCBzIAUZu7DFz09q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJ -BgNVBAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUw -EwYDVQQKEwxGb3J0LUZ1bnN0b24xETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQD -EwhjaGFuZ2VtZTERMA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1h -aWxAaG9zdC5kb21haW6CCQD9q+xqhCcEpzATBgNVHSUEDDAKBggrBgEFBQcDATAL -BgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQEFBQADgYEAQA8QOcS3Dw0vv9IWzI7TmvuL -zkt7DUh3zvH+1Y/qsXHtSR2fIzoW1HB8xSm/5JA00PAAJPTk3yzDgwFmYcmoqynn -mG0niUp2yS4Zjv5u1fiZEQ6XZ0s04x7jnzUApTL5tSzy4MUuzIG9GN1cEshr+gwX -dDBV9m4gmmweCbQMFUI= ------END CERTIFICATE----- diff --git a/integration-cli/fixtures/https/server-cert.pem b/integration-cli/fixtures/https/server-cert.pem new file mode 120000 index 0000000000..c18601067a --- /dev/null +++ b/integration-cli/fixtures/https/server-cert.pem @@ -0,0 +1 @@ +../../../integration/testdata/https/server-cert.pem \ No newline at end of file diff --git a/integration-cli/fixtures/https/server-key.pem b/integration-cli/fixtures/https/server-key.pem deleted file mode 100644 index c269320ef0..0000000000 --- a/integration-cli/fixtures/https/server-key.pem +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMH/fTBvZEqxkrFx -0cF04h3bLREk4QDUAK5vyJ6uZ7NKvffmnldtGUw8I5QtPdZjhNj6dis4EsHtIJ0y -4OjCv5p3cAQ/f8qMLILWPSVcAhpPZJMD3ZxCl14JSa/wwuEwCA4hRpXRE1nAyHa+ -lA2LQ2chM7IIYJ12qAUyHvmVCRR1AgMBAAECgYAmwckb9RUfSwyYgLm8IYLPHiuJ -wkllZfVg5Bo7gXJcQnFjZmJ56uTj8xvUjZlODIHM63TSO5ibv6kFXtXKCqZGd2M+ -wGbhZ0f+2GvKcwMmJERnIQjuoNaYSQLT0tM0VB9Iz0rJlZC+tzPZ+5pPqEumRdsS -IzWNXfF42AhcbwAQYQJBAPVXtMYIJc9EZsz86ZcQiMPWUpCX5vnRmtwL8kKyR8D5 -4KfYeiowyFffSRMMcclwNHq7TgSXN+nIXM9WyzyzwikCQQDKbNA28AgZp9aT54HP -WnbeE2pmt+uk/zl/BtxJSoK6H+69Jec+lf7EgL7HgOWYRSNot4uQWu8IhsHLTiUq -+0FtAkEAqwlRxRy4/x24bP+D+QRV0/D97j93joFJbE4Hved7jlSlAV4xDGilwlyv -HNB4Iu5OJ6Gcaibhm+FKkmD3noHSwQJBAIpu3fokLzX0bS+bDFBU6qO3HXX/47xj -+tsfQvkwZrSI8AkU6c8IX0HdVhsz0FBRQAT2ORDQz1XCarfxykNZrwUCQQCGCBIc -BBCWzhHlswlGidWJg3HqqO6hPPClEr3B5G87oCsdeYwiO23XT6rUnoJXfJHp6oCW -5nCwDu5ZTP+khltg ------END PRIVATE KEY----- diff --git a/integration-cli/fixtures/https/server-key.pem b/integration-cli/fixtures/https/server-key.pem new file mode 120000 index 0000000000..48b9c2df65 --- /dev/null +++ b/integration-cli/fixtures/https/server-key.pem @@ -0,0 +1 @@ +../../../integration/testdata/https/server-key.pem \ No newline at end of file diff --git a/integration/internal/api/container/container.go b/integration/internal/api/container/container.go new file mode 100644 index 0000000000..d65a9ea3b6 --- /dev/null +++ b/integration/internal/api/container/container.go @@ -0,0 +1,128 @@ +package container + +import ( + "context" + "io" + "os" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" +) + +// CreateVMemLabel creates a container with the supplied parameters +func CreateVMemLabel(client client.APIClient, v []string, memoryMB int, labels map[string]string, img string, cmd []string) (string, error) { + ctx := context.Background() + config := container.Config{ + Cmd: cmd, + Image: img, + Labels: labels, + } + hostConfig := container.HostConfig{ + Binds: v, + Resources: container.Resources{ + Memory: int64(memoryMB * 1024 * 1024), + }, + } + networkingConfig := networktypes.NetworkingConfig{} + name := "" + response, err := client.ContainerCreate(ctx, &config, &hostConfig, &networkingConfig, name) + + if err != nil { + return "", err + } + + return response.ID, nil +} + +// Run runs the command provided in the container image named +func Run(client client.APIClient, img string, cmd []string) (string, error) { + return RunV(client, []string{}, img, cmd) +} + +// RunV runs the command provided in the container image named with +// the equivalent of -v bind mounts +func RunV(client client.APIClient, v []string, img string, cmd []string) (string, error) { + return RunVMem(client, v, 0, img, cmd) +} + +// RunVMem runs the command provided in the container image named with +// the equivalent of -v bind mounts and a specified memory limit +func RunVMem(client client.APIClient, v []string, memoryMB int, img string, cmd []string) (string, error) { + return RunVMemLabel(client, v, memoryMB, map[string]string{}, img, cmd) +} + +// RunVLabel runs the command provided in the container image named +// with the equivalent of -v bind mounts and the specified labels +func RunVLabel(client client.APIClient, v []string, labels map[string]string, img string, cmd []string) (string, error) { + return RunVMemLabel(client, v, 0, labels, img, cmd) +} + +// RunVMemLabel runs the command provided in the container image named +// with the equivalent of -v bind mounts, a specified memory limit, +// and the specified labels +func RunVMemLabel(client client.APIClient, v []string, memoryMB int, labels map[string]string, img string, cmd []string) (string, error) { + containerID, err := CreateVMemLabel(client, v, memoryMB, labels, img, cmd) + if err != nil { + return "", err + } + + ctx := context.Background() + if err := client.ContainerStart(ctx, containerID, types.ContainerStartOptions{}); err != nil { + return "", err + } + + return containerID, nil +} + +// Wait waits until the named container has exited +func Wait(client client.APIClient, container string) (int64, error) { + resultC, errC := client.ContainerWait(context.Background(), container, "") + + select { + case result := <-resultC: + return result.StatusCode, nil + case err := <-errC: + return -1, err + } +} + +// Stop stops the named container +func Stop(client client.APIClient, container string) error { + timeout := time.Duration(10) * time.Second + ctx := context.Background() + return client.ContainerStop(ctx, container, &timeout) +} + +// Start starts the named container +func Start(client client.APIClient, container string) error { + ctx := context.Background() + return client.ContainerStart(ctx, container, types.ContainerStartOptions{}) +} + +// Kill kills the named container with SIGKILL +func Kill(client client.APIClient, container string) error { + ctx := context.Background() + return client.ContainerKill(ctx, container, "KILL") +} + +// Export exports a container's file system as a tarball +func Export(client client.APIClient, path, name string) error { + ctx := context.Background() + responseReader, err := client.ContainerExport(ctx, name) + if err != nil { + return err + } + defer responseReader.Close() + + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, responseReader) + return err +} diff --git a/integration/internal/api/image/image.go b/integration/internal/api/image/image.go new file mode 100644 index 0000000000..67ed7a0248 --- /dev/null +++ b/integration/internal/api/image/image.go @@ -0,0 +1,66 @@ +package image + +import ( + "context" + "io" + "os" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" +) + +// Save saves an image to a tarball names by path +func Save(client client.APIClient, path, image string) error { + ctx := context.Background() + responseReader, err := client.ImageSave(ctx, []string{image}) + if err != nil { + return err + } + defer responseReader.Close() + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(file, responseReader) + return err +} + +// Load loads an image from a tarball named by path +func Load(client client.APIClient, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + quiet := true + ctx := context.Background() + response, err := client.ImageLoad(ctx, file, quiet) + if err != nil { + return err + } + defer response.Body.Close() + return nil +} + +// Import imports the contents of a tarball named by path +func Import(client client.APIClient, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + options := types.ImageImportOptions{} + ref := "" + source := types.ImageImportSource{ + Source: file, + SourceName: "-", + } + ctx := context.Background() + responseReader, err := client.ImageImport(ctx, source, ref, options) + if err != nil { + return err + } + defer responseReader.Close() + return nil +} diff --git a/integration/internal/api/plugin/plugin.go b/integration/internal/api/plugin/plugin.go new file mode 100644 index 0000000000..cc771ec706 --- /dev/null +++ b/integration/internal/api/plugin/plugin.go @@ -0,0 +1,59 @@ +package plugin + +import ( + "context" + "io/ioutil" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" +) + +// InstallGrantAllPermissions installs the plugin named and grants it +// all permissions it may require +func InstallGrantAllPermissions(client client.APIClient, name string) error { + ctx := context.Background() + options := types.PluginInstallOptions{ + RemoteRef: name, + AcceptAllPermissions: true, + } + responseReader, err := client.PluginInstall(ctx, "", options) + if err != nil { + return err + } + defer responseReader.Close() + // we have to read the response out here because the client API + // actually starts a goroutine which we can only be sure has + // completed when we get EOF from reading responseBody + _, err = ioutil.ReadAll(responseReader) + return err +} + +// Enable enables the named plugin +func Enable(client client.APIClient, name string) error { + ctx := context.Background() + options := types.PluginEnableOptions{} + return client.PluginEnable(ctx, name, options) +} + +// Disable disables the named plugin +func Disable(client client.APIClient, name string) error { + ctx := context.Background() + options := types.PluginDisableOptions{} + return client.PluginDisable(ctx, name, options) +} + +// Rm removes the named plugin +func Rm(client client.APIClient, name string) error { + return remove(client, name, false) +} + +// RmF forces the removal of the named plugin +func RmF(client client.APIClient, name string) error { + return remove(client, name, true) +} + +func remove(client client.APIClient, name string, force bool) error { + ctx := context.Background() + options := types.PluginRemoveOptions{Force: force} + return client.PluginRemove(ctx, name, options) +} diff --git a/integration/internal/api/system/system.go b/integration/internal/api/system/system.go new file mode 100644 index 0000000000..6983479636 --- /dev/null +++ b/integration/internal/api/system/system.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" + "github.com/docker/docker/client" + "github.com/docker/docker/internal/test/environment" + "github.com/stretchr/testify/require" +) + +// Time provides the current time on the daemon host +func Time(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time { + if testEnv.IsLocalDaemon() { + return time.Now() + } + + ctx := context.Background() + info, err := client.Info(ctx) + require.Nil(t, err) + + dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) + require.Nil(t, err, "invalid time format in GET /info response") + return dt +} + +// Version provides the version of the daemon +func Version(client client.APIClient) (types.Version, error) { + ctx := context.Background() + return client.ServerVersion(ctx) +} + +// EventsSince returns event and error streams since a provided time +func EventsSince(client client.APIClient, since string) (<-chan events.Message, <-chan error, func()) { + eventOptions := types.EventsOptions{ + Since: since, + } + ctx, cancel := context.WithCancel(context.Background()) + events, errs := client.Events(ctx, eventOptions) + + return events, errs, cancel +} diff --git a/integration/internal/api/volume/volume.go b/integration/internal/api/volume/volume.go new file mode 100644 index 0000000000..3f07645f06 --- /dev/null +++ b/integration/internal/api/volume/volume.go @@ -0,0 +1,61 @@ +package volume + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" +) + +// Create creates a volume using the named driver with the specified options +func Create(client client.APIClient, driver string, opts map[string]string) (string, error) { + volReq := volumetypes.VolumesCreateBody{ + Driver: driver, + DriverOpts: opts, + Name: "", + } + + ctx := context.Background() + vol, err := client.VolumeCreate(ctx, volReq) + if err != nil { + return "", err + } + return vol.Name, nil +} + +// Rm removes the volume named +func Rm(client client.APIClient, name string) error { + ctx := context.Background() + return client.VolumeRemove(ctx, name, false) +} + +// Ls lists the volumes available +func Ls(client client.APIClient) ([]string, error) { + ctx := context.Background() + volumes, err := client.VolumeList(ctx, filters.Args{}) + if err != nil { + return []string{}, err + } + + names := []string{} + for _, volume := range volumes.Volumes { + names = append(names, volume.Name) + } + + return names, nil +} + +// Prune removes all volumes not used by at least one container +func Prune(client client.APIClient) (types.VolumesPruneReport, error) { + ctx := context.Background() + + return client.VolumesPrune(ctx, filters.Args{}) +} + +// Inspect retrieves detailed information about the named volume +func Inspect(client client.APIClient, name string) (types.Volume, error) { + ctx := context.Background() + return client.VolumeInspect(ctx, name) +} diff --git a/integration/plugin/authz/authz_plugin_test.go b/integration/plugin/authz/authz_plugin_test.go new file mode 100644 index 0000000000..49bb3e0d21 --- /dev/null +++ b/integration/plugin/authz/authz_plugin_test.go @@ -0,0 +1,370 @@ +// +build !windows + +package authz + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + eventtypes "github.com/docker/docker/api/types/events" + "github.com/docker/docker/integration/internal/api/container" + "github.com/docker/docker/integration/internal/api/image" + "github.com/docker/docker/integration/internal/api/system" + "github.com/docker/docker/integration/util/request" + "github.com/docker/docker/pkg/authorization" + "github.com/gotestyourself/gotestyourself/skip" + "github.com/stretchr/testify/require" +) + +const ( + testAuthZPlugin = "authzplugin" + unauthorizedMessage = "User unauthorized authz plugin" + errorMessage = "something went wrong..." + serverVersionAPI = "/version" +) + +var ( + alwaysAllowed = []string{"/_ping", "/info"} + ctrl *authorizationController +) + +type authorizationController struct { + reqRes authorization.Response // reqRes holds the plugin response to the initial client request + resRes authorization.Response // resRes holds the plugin response to the daemon response + versionReqCount int // versionReqCount counts the number of requests to the server version API endpoint + versionResCount int // versionResCount counts the number of responses from the server version API endpoint + requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller + reqUser string + resUser string +} + +func setupTestV1(t *testing.T) func() { + ctrl = &authorizationController{} + teardown := setupTest(t) + + err := os.MkdirAll("/etc/docker/plugins", 0755) + require.Nil(t, err) + + fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin) + err = ioutil.WriteFile(fileName, []byte(server.URL), 0644) + require.Nil(t, err) + + return func() { + err := os.RemoveAll("/etc/docker/plugins") + require.Nil(t, err) + + teardown() + ctrl = nil + } +} + +// check for always allowed endpoints to not inhibit test framework functions +func isAllowed(reqURI string) bool { + for _, endpoint := range alwaysAllowed { + if strings.HasSuffix(reqURI, endpoint) { + return true + } + } + return false +} + +func TestAuthZPluginAllowRequest(t *testing.T) { + defer setupTestV1(t)() + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin) + + client, err := d.NewClient() + require.Nil(t, err) + + // Ensure command successful + id, err := container.Run(client, "busybox", []string{"top"}) + require.Nil(t, err) + + assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") + assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id)) + + _, err = system.Version(client) + require.Nil(t, err) + require.Equal(t, 1, ctrl.versionReqCount) + require.Equal(t, 1, ctrl.versionResCount) +} + +func TestAuthZPluginTLS(t *testing.T) { + defer setupTestV1(t)() + const ( + testDaemonHTTPSAddr = "tcp://localhost:4271" + cacertPath = "../../testdata/https/ca.pem" + serverCertPath = "../../testdata/https/server-cert.pem" + serverKeyPath = "../../testdata/https/server-key.pem" + clientCertPath = "../../testdata/https/client-cert.pem" + clientKeyPath = "../../testdata/https/client-key.pem" + ) + + d.Start(t, + "--authorization-plugin="+testAuthZPlugin, + "--tlsverify", + "--tlscacert", cacertPath, + "--tlscert", serverCertPath, + "--tlskey", serverKeyPath, + "-H", testDaemonHTTPSAddr) + + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + + client, err := request.NewTLSAPIClient(t, testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath) + require.Nil(t, err) + + _, err = system.Version(client) + require.Nil(t, err) + + require.Equal(t, "client", ctrl.reqUser) + require.Equal(t, "client", ctrl.resUser) +} + +func TestAuthZPluginDenyRequest(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin) + ctrl.reqRes.Allow = false + ctrl.reqRes.Msg = unauthorizedMessage + + client, err := d.NewClient() + require.Nil(t, err) + + // Ensure command is blocked + _, err = system.Version(client) + require.NotNil(t, err) + require.Equal(t, 1, ctrl.versionReqCount) + require.Equal(t, 0, ctrl.versionResCount) + + // Ensure unauthorized message appears in response + require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) +} + +// TestAuthZPluginAPIDenyResponse validates that when authorization +// plugin deny the request, the status code is forbidden +func TestAuthZPluginAPIDenyResponse(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin) + ctrl.reqRes.Allow = false + ctrl.resRes.Msg = unauthorizedMessage + + daemonURL, err := url.Parse(d.Sock()) + require.Nil(t, err) + + conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) + require.Nil(t, err) + client := httputil.NewClientConn(conn, nil) + req, err := http.NewRequest("GET", "/version", nil) + require.Nil(t, err) + resp, err := client.Do(req) + + require.Nil(t, err) + require.Equal(t, http.StatusForbidden, resp.StatusCode) +} + +func TestAuthZPluginDenyResponse(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin) + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = false + ctrl.resRes.Msg = unauthorizedMessage + + client, err := d.NewClient() + require.Nil(t, err) + + // Ensure command is blocked + _, err = system.Version(client) + require.NotNil(t, err) + require.Equal(t, 1, ctrl.versionReqCount) + require.Equal(t, 1, ctrl.versionResCount) + + // Ensure unauthorized message appears in response + require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) +} + +// TestAuthZPluginAllowEventStream verifies event stream propagates +// correctly after request pass through by the authorization plugin +func TestAuthZPluginAllowEventStream(t *testing.T) { + skip.IfCondition(t, testEnv.DaemonInfo.OSType != "linux") + + defer setupTestV1(t)() + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin) + + client, err := d.NewClient() + require.Nil(t, err) + + startTime := strconv.FormatInt(system.Time(t, client, testEnv).Unix(), 10) + events, errs, cancel := system.EventsSince(client, startTime) + defer cancel() + + // Create a container and wait for the creation events + id, err := container.Run(client, "busybox", []string{"top"}) + require.Nil(t, err) + for i := 0; i < 100; i++ { + c, err := client.ContainerInspect(context.Background(), id) + require.Nil(t, err) + if c.State.Running { + break + } + if i == 99 { + t.Fatal("Container didn't run within 10s") + } + time.Sleep(100 * time.Millisecond) + } + + created := false + started := false + for !created && !started { + select { + case event := <-events: + if event.Type == eventtypes.ContainerEventType && event.Actor.ID == id { + if event.Action == "create" { + created = true + } + if event.Action == "start" { + started = true + } + } + case err := <-errs: + if err == io.EOF { + t.Fatal("premature end of event stream") + } + require.Nil(t, err) + case <-time.After(30 * time.Second): + // Fail the test + t.Fatal("event stream timeout") + } + } + + // Ensure both events and container endpoints are passed to the + // authorization plugin + assertURIRecorded(t, ctrl.requestsURIs, "/events") + assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") + assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id)) +} + +func TestAuthZPluginErrorResponse(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin) + ctrl.reqRes.Allow = true + ctrl.resRes.Err = errorMessage + + client, err := d.NewClient() + require.Nil(t, err) + + // Ensure command is blocked + _, err = system.Version(client) + require.NotNil(t, err) + require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error()) +} + +func TestAuthZPluginErrorRequest(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin) + ctrl.reqRes.Err = errorMessage + + client, err := d.NewClient() + require.Nil(t, err) + + // Ensure command is blocked + _, err = system.Version(client) + require.NotNil(t, err) + require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error()) +} + +func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) { + defer setupTestV1(t)() + d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) + + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + + client, err := d.NewClient() + require.Nil(t, err) + + _, err = system.Version(client) + require.Nil(t, err) + + // assert plugin is only called once.. + require.Equal(t, 1, ctrl.versionReqCount) + require.Equal(t, 1, ctrl.versionResCount) +} + +func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) { + defer setupTestV1(t)() + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) + + client, err := d.NewClient() + require.Nil(t, err) + + tmp, err := ioutil.TempDir("", "test-authz-load-import") + require.Nil(t, err) + defer os.RemoveAll(tmp) + + savedImagePath := filepath.Join(tmp, "save.tar") + + err = image.Save(client, savedImagePath, "busybox") + require.Nil(t, err) + err = image.Load(client, savedImagePath) + require.Nil(t, err) + + exportedImagePath := filepath.Join(tmp, "export.tar") + + id, err := container.Run(client, "busybox", []string{}) + require.Nil(t, err) + err = container.Export(client, exportedImagePath, id) + require.Nil(t, err) + err = image.Import(client, exportedImagePath) + require.Nil(t, err) +} + +func TestAuthZPluginHeader(t *testing.T) { + defer setupTestV1(t)() + ctrl.reqRes.Allow = true + ctrl.resRes.Allow = true + d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin) + + daemonURL, err := url.Parse(d.Sock()) + require.Nil(t, err) + + conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) + require.Nil(t, err) + client := httputil.NewClientConn(conn, nil) + req, err := http.NewRequest("GET", "/version", nil) + require.Nil(t, err) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, "application/json", resp.Header["Content-Type"][0]) +} + +// assertURIRecorded verifies that the given URI was sent and recorded +// in the authz plugin +func assertURIRecorded(t *testing.T, uris []string, uri string) { + var found bool + for _, u := range uris { + if strings.Contains(u, uri) { + found = true + break + } + } + if !found { + t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ",")) + } +} diff --git a/integration/plugin/authz/authz_plugin_v2_test.go b/integration/plugin/authz/authz_plugin_v2_test.go new file mode 100644 index 0000000000..6f8a934b38 --- /dev/null +++ b/integration/plugin/authz/authz_plugin_v2_test.go @@ -0,0 +1,153 @@ +// +build !windows + +package authz + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + + "github.com/docker/docker/integration/internal/api/container" + "github.com/docker/docker/integration/internal/api/plugin" + "github.com/docker/docker/integration/internal/api/volume" + "github.com/docker/docker/integration/util/requirement" + "github.com/gotestyourself/gotestyourself/skip" + "github.com/stretchr/testify/require" +) + +var ( + authzPluginName = "riyaz/authz-no-volume-plugin" + authzPluginTag = "latest" + authzPluginNameWithTag = authzPluginName + ":" + authzPluginTag + authzPluginBadManifestName = "riyaz/authz-plugin-bad-manifest" + nonexistentAuthzPluginName = "riyaz/nonexistent-authz-plugin" +) + +func setupTestV2(t *testing.T) func() { + skip.IfCondition(t, testEnv.DaemonInfo.OSType != "linux") + requirement.HasHubConnectivity(t) + + teardown := setupTest(t) + + d.Start(t) + + return teardown +} + +func TestAuthZPluginV2AllowNonVolumeRequest(t *testing.T) { + skip.IfCondition(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64") + defer setupTestV2(t)() + + client, err := d.NewClient() + require.Nil(t, err) + + // Install authz plugin + err = plugin.InstallGrantAllPermissions(client, authzPluginNameWithTag) + require.Nil(t, err) + // start the daemon with the plugin and load busybox, --net=none build fails otherwise + // because it needs to pull busybox + d.Restart(t, "--authorization-plugin="+authzPluginNameWithTag) + d.LoadBusybox(t) + + // Ensure docker run command and accompanying docker ps are successful + id, err := container.Run(client, "busybox", []string{"top"}) + require.Nil(t, err) + + _, err = client.ContainerInspect(context.Background(), id) + require.Nil(t, err) +} + +func TestAuthZPluginV2Disable(t *testing.T) { + skip.IfCondition(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64") + defer setupTestV2(t)() + + client, err := d.NewClient() + require.Nil(t, err) + + // Install authz plugin + err = plugin.InstallGrantAllPermissions(client, authzPluginNameWithTag) + require.Nil(t, err) + + d.Restart(t, "--authorization-plugin="+authzPluginNameWithTag) + d.LoadBusybox(t) + + _, err = volume.Create(client, "local", map[string]string{}) + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) + + // disable the plugin + err = plugin.Disable(client, authzPluginNameWithTag) + require.Nil(t, err) + + // now test to see if the docker api works. + _, err = volume.Create(client, "local", map[string]string{}) + require.Nil(t, err) +} + +func TestAuthZPluginV2RejectVolumeRequests(t *testing.T) { + skip.IfCondition(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64") + defer setupTestV2(t)() + + client, err := d.NewClient() + require.Nil(t, err) + + // Install authz plugin + err = plugin.InstallGrantAllPermissions(client, authzPluginNameWithTag) + require.Nil(t, err) + + // restart the daemon with the plugin + d.Restart(t, "--authorization-plugin="+authzPluginNameWithTag) + + _, err = volume.Create(client, "local", map[string]string{}) + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) + + _, err = volume.Ls(client) + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) + + // The plugin will block the command before it can determine the volume does not exist + err = volume.Rm(client, "test") + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) + + _, err = volume.Inspect(client, "test") + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) + + _, err = volume.Prune(client) + require.NotNil(t, err) + require.True(t, strings.Contains(err.Error(), fmt.Sprintf("Error response from daemon: plugin %s failed with error:", authzPluginNameWithTag))) +} + +func TestAuthZPluginV2BadManifestFailsDaemonStart(t *testing.T) { + skip.IfCondition(t, os.Getenv("DOCKER_ENGINE_GOARCH") != "amd64") + defer setupTestV2(t)() + + client, err := d.NewClient() + require.Nil(t, err) + + // Install authz plugin with bad manifest + err = plugin.InstallGrantAllPermissions(client, authzPluginBadManifestName) + require.Nil(t, err) + + // start the daemon with the plugin, it will error + err = d.RestartWithError("--authorization-plugin=" + authzPluginBadManifestName) + require.NotNil(t, err) + + // restarting the daemon without requiring the plugin will succeed + d.Start(t) +} + +func TestAuthZPluginV2NonexistentFailsDaemonStart(t *testing.T) { + defer setupTestV2(t)() + + // start the daemon with a non-existent authz plugin, it will error + err := d.RestartWithError("--authorization-plugin=" + nonexistentAuthzPluginName) + require.NotNil(t, err) + + // restarting the daemon without requiring the plugin will succeed + d.Start(t) +} diff --git a/integration/plugin/authz/main_test.go b/integration/plugin/authz/main_test.go new file mode 100644 index 0000000000..da1799ea1b --- /dev/null +++ b/integration/plugin/authz/main_test.go @@ -0,0 +1,177 @@ +// +build !windows + +package authz + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/docker/docker/integration-cli/daemon" + "github.com/docker/docker/internal/test/environment" + "github.com/docker/docker/pkg/authorization" + "github.com/docker/docker/pkg/plugins" +) + +var ( + testEnv *environment.Execution + d *daemon.Daemon + server *httptest.Server +) + +const dockerdBinary = "dockerd" + +func TestMain(m *testing.M) { + var err error + testEnv, err = environment.New() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + testEnv.Print() + setupSuite() + exitCode := m.Run() + teardownSuite() + + os.Exit(exitCode) +} + +func setupTest(t *testing.T) func() { + environment.ProtectAll(t, testEnv) + + d = daemon.New(t, "", dockerdBinary, daemon.Config{ + Experimental: testEnv.DaemonInfo.ExperimentalBuild, + }) + + return func() { + if d != nil { + d.Stop(t) + } + testEnv.Clean(t) + } +} + +func setupSuite() { + mux := http.NewServeMux() + server = httptest.NewServer(mux) + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}}) + if err != nil { + panic("could not marshal json for /Plugin.Activate: " + err.Error()) + } + w.Write(b) + }) + + mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic("could not read body for /AuthZPlugin.AuthZReq: " + err.Error()) + } + authReq := authorization.Request{} + err = json.Unmarshal(body, &authReq) + if err != nil { + panic("could not unmarshal json for /AuthZPlugin.AuthZReq: " + err.Error()) + } + + assertBody(authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody) + assertAuthHeaders(authReq.RequestHeaders) + + // Count only server version api + if strings.HasSuffix(authReq.RequestURI, serverVersionAPI) { + ctrl.versionReqCount++ + } + + ctrl.requestsURIs = append(ctrl.requestsURIs, authReq.RequestURI) + + reqRes := ctrl.reqRes + if isAllowed(authReq.RequestURI) { + reqRes = authorization.Response{Allow: true} + } + if reqRes.Err != "" { + w.WriteHeader(http.StatusInternalServerError) + } + b, err := json.Marshal(reqRes) + if err != nil { + panic("could not marshal json for /AuthZPlugin.AuthZReq: " + err.Error()) + } + + ctrl.reqUser = authReq.User + w.Write(b) + }) + + mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic("could not read body for /AuthZPlugin.AuthZRes: " + err.Error()) + } + authReq := authorization.Request{} + err = json.Unmarshal(body, &authReq) + if err != nil { + panic("could not unmarshal json for /AuthZPlugin.AuthZRes: " + err.Error()) + } + + assertBody(authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody) + assertAuthHeaders(authReq.ResponseHeaders) + + // Count only server version api + if strings.HasSuffix(authReq.RequestURI, serverVersionAPI) { + ctrl.versionResCount++ + } + resRes := ctrl.resRes + if isAllowed(authReq.RequestURI) { + resRes = authorization.Response{Allow: true} + } + if resRes.Err != "" { + w.WriteHeader(http.StatusInternalServerError) + } + b, err := json.Marshal(resRes) + if err != nil { + panic("could not marshal json for /AuthZPlugin.AuthZRes: " + err.Error()) + } + ctrl.resUser = authReq.User + w.Write(b) + }) +} + +func teardownSuite() { + if server == nil { + return + } + + server.Close() +} + +// assertAuthHeaders validates authentication headers are removed +func assertAuthHeaders(headers map[string]string) error { + for k := range headers { + if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") { + panic(fmt.Sprintf("Found authentication headers in request '%v'", headers)) + } + } + return nil +} + +// assertBody asserts that body is removed for non text/json requests +func assertBody(requestURI string, headers map[string]string, body []byte) { + if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 { + panic("Body included for authentication endpoint " + string(body)) + } + + for k, v := range headers { + if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" { + return + } + } + if len(body) > 0 { + panic(fmt.Sprintf("Body included while it should not (Headers: '%v')", headers)) + } +} diff --git a/integration/testdata/https/ca.pem b/integration/testdata/https/ca.pem new file mode 100644 index 0000000000..6825d6d1bd --- /dev/null +++ b/integration/testdata/https/ca.pem @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID0TCCAzqgAwIBAgIJAP2r7GqEJwSnMA0GCSqGSIb3DQEBBQUAMIGiMQswCQYD +VQQGEwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMG +A1UEChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTERMA8GA1UEAxMI +Y2hhbmdlbWUxETAPBgNVBCkTCGNoYW5nZW1lMR8wHQYJKoZIhvcNAQkBFhBtYWls +QGhvc3QuZG9tYWluMB4XDTEzMTIwMzE2NTYzMFoXDTIzMTIwMTE2NTYzMFowgaIx +CzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2Nv +MRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24xETAPBgNVBAsTCGNoYW5nZW1lMREwDwYD +VQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEW +EG1haWxAaG9zdC5kb21haW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALAn +0xDw+5y7ZptQacq66pUhRu82JP2WU6IDgo5QUtNU6/CX5PwQATe/OnYTZQFbksxp +AU9boG0FCkgxfsgPYXEuZxVEGKI2fxfKHOZZI8mrkWmj6eWU/0cvCjGVc9rTITP5 +sNQvg+hORyVDdNp2IdsbMJayiB3AQYMFx3vSDOMTAgMBAAGjggELMIIBBzAdBgNV +HQ4EFgQUZu7DFz09q0QBa2+ymRm9qgK1NPswgdcGA1UdIwSBzzCBzIAUZu7DFz09 +q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJD +QTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24x +ETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMI +Y2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21haW6CCQD9q+xq +hCcEpzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAF8fJKKM+/oOdnNi +zEd0M1+PmZOyqvjYQn/2ZR8UHH6Imgc/OPQKZXf0bVE1Txc/DaUNn9Isd1SuCuaE +ic3vAIYYU7PmgeNN6vwec48V96T7jr+GAi6AVMhQEc2hHCfVtx11Xx+x6aHDZzJt +Zxtf5lL6KSO9Y+EFwM+rju6hm5hW +-----END CERTIFICATE----- diff --git a/integration/testdata/https/client-cert.pem b/integration/testdata/https/client-cert.pem new file mode 100644 index 0000000000..c05ed47c2c --- /dev/null +++ b/integration/testdata/https/client-cert.pem @@ -0,0 +1,73 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 3 (0x3) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=changeme/name=changeme/emailAddress=mail@host.domain + Validity + Not Before: Dec 4 14:17:54 2013 GMT + Not After : Dec 2 14:17:54 2023 GMT + Subject: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=client/name=changeme/emailAddress=mail@host.domain + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:ca:c9:05:d0:09:4e:3e:a4:fc:d5:14:f4:a5:e8: + 34:d3:6b:51:e3:f3:62:ea:a1:f0:e8:ed:c4:2a:bc: + f0:4f:ca:07:df:e3:88:fa:f4:21:99:35:0e:3d:ea: + b0:86:e7:c4:d2:8a:83:2b:42:b8:ec:a3:99:62:70: + 81:46:cc:fc:a5:1d:d2:63:e8:eb:07:25:9a:e2:25: + 6d:11:56:f2:1a:51:a1:b6:3e:1c:57:32:e9:7b:2c: + aa:1b:cc:97:2d:89:2d:b1:c9:5e:35:28:4d:7c:fa: + 65:31:3e:f7:70:dd:6e:0b:3c:58:af:a8:2e:24:c0: + 7e:4e:78:7d:0a:9e:8f:42:43 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + Easy-RSA Generated Certificate + X509v3 Subject Key Identifier: + DE:42:EF:2D:98:A3:6C:A8:AA:E0:8C:71:2C:9D:64:23:A9:E2:7E:81 + X509v3 Authority Key Identifier: + keyid:66:EE:C3:17:3D:3D:AB:44:01:6B:6F:B2:99:19:BD:AA:02:B5:34:FB + DirName:/C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=changeme/CN=changeme/name=changeme/emailAddress=mail@host.domain + serial:FD:AB:EC:6A:84:27:04:A7 + + X509v3 Extended Key Usage: + TLS Web Client Authentication + X509v3 Key Usage: + Digital Signature + Signature Algorithm: sha1WithRSAEncryption + 1c:44:26:ea:e1:66:25:cb:e4:8e:57:1c:f6:b9:17:22:62:40: + 12:90:8f:3b:b2:61:7a:54:94:8f:b1:20:0b:bf:a3:51:e3:fa: + 1c:a1:be:92:3a:d0:76:44:c0:57:83:ab:6a:e4:1a:45:49:a4: + af:39:0d:60:32:fc:3a:be:d7:fb:5d:99:7a:1f:87:e7:d5:ab: + 84:a2:5e:90:d8:bf:fa:89:6d:32:26:02:5e:31:35:68:7f:31: + f5:6b:51:46:bc:af:70:ed:5a:09:7d:ec:b2:48:4f:fe:c5:2f: + 56:04:ad:f6:c1:d2:2a:e4:6a:c4:87:fe:08:35:c5:38:cb:5e: + 4a:c4 +-----BEGIN CERTIFICATE----- +MIIEFTCCA36gAwIBAgIBAzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZv +cnQtRnVuc3RvbjERMA8GA1UECxMIY2hhbmdlbWUxETAPBgNVBAMTCGNoYW5nZW1l +MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0LmRv +bWFpbjAeFw0xMzEyMDQxNDE3NTRaFw0yMzEyMDIxNDE3NTRaMIGgMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UE +ChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTEPMA0GA1UEAxMGY2xp +ZW50MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0 +LmRvbWFpbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAyskF0AlOPqT81RT0 +peg002tR4/Ni6qHw6O3EKrzwT8oH3+OI+vQhmTUOPeqwhufE0oqDK0K47KOZYnCB +Rsz8pR3SY+jrByWa4iVtEVbyGlGhtj4cVzLpeyyqG8yXLYktscleNShNfPplMT73 +cN1uCzxYr6guJMB+Tnh9Cp6PQkMCAwEAAaOCAVkwggFVMAkGA1UdEwQCMAAwLQYJ +YIZIAYb4QgENBCAWHkVhc3ktUlNBIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV +HQ4EFgQU3kLvLZijbKiq4IxxLJ1kI6nifoEwgdcGA1UdIwSBzzCBzIAUZu7DFz09 +q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJD +QTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUwEwYDVQQKEwxGb3J0LUZ1bnN0b24x +ETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQDEwhjaGFuZ2VtZTERMA8GA1UEKRMI +Y2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21haW6CCQD9q+xq +hCcEpzATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNVHQ8EBAMCB4AwDQYJKoZIhvcN +AQEFBQADgYEAHEQm6uFmJcvkjlcc9rkXImJAEpCPO7JhelSUj7EgC7+jUeP6HKG+ +kjrQdkTAV4OrauQaRUmkrzkNYDL8Or7X+12Zeh+H59WrhKJekNi/+oltMiYCXjE1 +aH8x9WtRRryvcO1aCX3sskhP/sUvVgSt9sHSKuRqxIf+CDXFOMteSsQ= +-----END CERTIFICATE----- diff --git a/integration/testdata/https/client-key.pem b/integration/testdata/https/client-key.pem new file mode 100644 index 0000000000..b5c15f8dc7 --- /dev/null +++ b/integration/testdata/https/client-key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMrJBdAJTj6k/NUU +9KXoNNNrUePzYuqh8OjtxCq88E/KB9/jiPr0IZk1Dj3qsIbnxNKKgytCuOyjmWJw +gUbM/KUd0mPo6wclmuIlbRFW8hpRobY+HFcy6XssqhvMly2JLbHJXjUoTXz6ZTE+ +93Ddbgs8WK+oLiTAfk54fQqej0JDAgMBAAECgYBOFEzKp2qbMEexe9ofL2N3rDDh +xkrl8OijpzkLA6i78BxMFn4dsnZlWUpciMrjhsYAExkiRRSS+QMMJimAq1jzQqc3 +FAQV2XGYwkd0cUn7iZGvfNnEPysjsfyYQM+m+sT0ATj4BZjVShC6kkSjTdm1leLN +OSvcHdcu3Xxg9ufF0QJBAPYdnNt5sIndt2WECePuRVi+uF4mlxTobFY0fjn26yhC +4RsnhhD3Vldygo9gvnkwrAZYaALGSPBewes2InxvjA8CQQDS7erKiNXpwoqz5XiU +SVEsIIVTdWzBjGbIqMOu/hUwM5FK4j6JTBks0aTGMyh0YV9L1EzM0X79J29JahCe +iQKNAkBKNMOGqTpBV0hko1sYDk96YobUXG5RL4L6uvkUIQ7mJMQam+AgXXL7Ctuy +v0iu4a38e8tgisiTMP7nHHtpaXihAkAOiN54/lzfMsykANgCP9scE1GcoqbP34Dl +qttxH4kOPT9xzY1JoLjLYdbc4YGUI3GRpBt2sajygNkmUey7P+2xAkBBsVCZFvTw +qHvOpPS2kX5ml5xoc/QAHK9N7kR+X7XFYx82RTVSqJEK4lPb+aEWn+CjiIewO4Q5 +ksDFuNxAzbhl +-----END PRIVATE KEY----- diff --git a/integration/testdata/https/server-cert.pem b/integration/testdata/https/server-cert.pem new file mode 100644 index 0000000000..08abfd1a3b --- /dev/null +++ b/integration/testdata/https/server-cert.pem @@ -0,0 +1,76 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4 (0x4) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=changeme/name=changeme/emailAddress=mail@host.domain + Validity + Not Before: Dec 4 15:01:20 2013 GMT + Not After : Dec 2 15:01:20 2023 GMT + Subject: C=US, ST=CA, L=SanFrancisco, O=Fort-Funston, OU=changeme, CN=*/name=changeme/emailAddress=mail@host.domain + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:c1:ff:7d:30:6f:64:4a:b1:92:b1:71:d1:c1:74: + e2:1d:db:2d:11:24:e1:00:d4:00:ae:6f:c8:9e:ae: + 67:b3:4a:bd:f7:e6:9e:57:6d:19:4c:3c:23:94:2d: + 3d:d6:63:84:d8:fa:76:2b:38:12:c1:ed:20:9d:32: + e0:e8:c2:bf:9a:77:70:04:3f:7f:ca:8c:2c:82:d6: + 3d:25:5c:02:1a:4f:64:93:03:dd:9c:42:97:5e:09: + 49:af:f0:c2:e1:30:08:0e:21:46:95:d1:13:59:c0: + c8:76:be:94:0d:8b:43:67:21:33:b2:08:60:9d:76: + a8:05:32:1e:f9:95:09:14:75 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Cert Type: + SSL Server + Netscape Comment: + Easy-RSA Generated Server Certificate + X509v3 Subject Key Identifier: + 14:02:FD:FD:DD:13:38:E0:71:EA:D1:BE:C0:0E:89:1A:2D:B6:19:06 + X509v3 Authority Key Identifier: + keyid:66:EE:C3:17:3D:3D:AB:44:01:6B:6F:B2:99:19:BD:AA:02:B5:34:FB + DirName:/C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=changeme/CN=changeme/name=changeme/emailAddress=mail@host.domain + serial:FD:AB:EC:6A:84:27:04:A7 + + X509v3 Extended Key Usage: + TLS Web Server Authentication + X509v3 Key Usage: + Digital Signature, Key Encipherment + Signature Algorithm: sha1WithRSAEncryption + 40:0f:10:39:c4:b7:0f:0d:2f:bf:d2:16:cc:8e:d3:9a:fb:8b: + ce:4b:7b:0d:48:77:ce:f1:fe:d5:8f:ea:b1:71:ed:49:1d:9f: + 23:3a:16:d4:70:7c:c5:29:bf:e4:90:34:d0:f0:00:24:f4:e4: + df:2c:c3:83:01:66:61:c9:a8:ab:29:e7:98:6d:27:89:4a:76: + c9:2e:19:8e:fe:6e:d5:f8:99:11:0e:97:67:4b:34:e3:1e:e3: + 9f:35:00:a5:32:f9:b5:2c:f2:e0:c5:2e:cc:81:bd:18:dd:5c: + 12:c8:6b:fa:0c:17:74:30:55:f6:6e:20:9a:6c:1e:09:b4:0c: + 15:42 +-----BEGIN CERTIFICATE----- +MIIEKjCCA5OgAwIBAgIBBDANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMCVVMx +CzAJBgNVBAgTAkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZv +cnQtRnVuc3RvbjERMA8GA1UECxMIY2hhbmdlbWUxETAPBgNVBAMTCGNoYW5nZW1l +MREwDwYDVQQpEwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0LmRv +bWFpbjAeFw0xMzEyMDQxNTAxMjBaFw0yMzEyMDIxNTAxMjBaMIGbMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCQ0ExFTATBgNVBAcTDFNhbkZyYW5jaXNjbzEVMBMGA1UE +ChMMRm9ydC1GdW5zdG9uMREwDwYDVQQLEwhjaGFuZ2VtZTEKMAgGA1UEAxQBKjER +MA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1haWxAaG9zdC5kb21h +aW4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMH/fTBvZEqxkrFx0cF04h3b +LREk4QDUAK5vyJ6uZ7NKvffmnldtGUw8I5QtPdZjhNj6dis4EsHtIJ0y4OjCv5p3 +cAQ/f8qMLILWPSVcAhpPZJMD3ZxCl14JSa/wwuEwCA4hRpXRE1nAyHa+lA2LQ2ch +M7IIYJ12qAUyHvmVCRR1AgMBAAGjggFzMIIBbzAJBgNVHRMEAjAAMBEGCWCGSAGG ++EIBAQQEAwIGQDA0BglghkgBhvhCAQ0EJxYlRWFzeS1SU0EgR2VuZXJhdGVkIFNl +cnZlciBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUFAL9/d0TOOBx6tG+wA6JGi22GQYw +gdcGA1UdIwSBzzCBzIAUZu7DFz09q0QBa2+ymRm9qgK1NPuhgaikgaUwgaIxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIEwJDQTEVMBMGA1UEBxMMU2FuRnJhbmNpc2NvMRUw +EwYDVQQKEwxGb3J0LUZ1bnN0b24xETAPBgNVBAsTCGNoYW5nZW1lMREwDwYDVQQD +EwhjaGFuZ2VtZTERMA8GA1UEKRMIY2hhbmdlbWUxHzAdBgkqhkiG9w0BCQEWEG1h +aWxAaG9zdC5kb21haW6CCQD9q+xqhCcEpzATBgNVHSUEDDAKBggrBgEFBQcDATAL +BgNVHQ8EBAMCBaAwDQYJKoZIhvcNAQEFBQADgYEAQA8QOcS3Dw0vv9IWzI7TmvuL +zkt7DUh3zvH+1Y/qsXHtSR2fIzoW1HB8xSm/5JA00PAAJPTk3yzDgwFmYcmoqynn +mG0niUp2yS4Zjv5u1fiZEQ6XZ0s04x7jnzUApTL5tSzy4MUuzIG9GN1cEshr+gwX +dDBV9m4gmmweCbQMFUI= +-----END CERTIFICATE----- diff --git a/integration/testdata/https/server-key.pem b/integration/testdata/https/server-key.pem new file mode 100644 index 0000000000..c269320ef0 --- /dev/null +++ b/integration/testdata/https/server-key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMH/fTBvZEqxkrFx +0cF04h3bLREk4QDUAK5vyJ6uZ7NKvffmnldtGUw8I5QtPdZjhNj6dis4EsHtIJ0y +4OjCv5p3cAQ/f8qMLILWPSVcAhpPZJMD3ZxCl14JSa/wwuEwCA4hRpXRE1nAyHa+ +lA2LQ2chM7IIYJ12qAUyHvmVCRR1AgMBAAECgYAmwckb9RUfSwyYgLm8IYLPHiuJ +wkllZfVg5Bo7gXJcQnFjZmJ56uTj8xvUjZlODIHM63TSO5ibv6kFXtXKCqZGd2M+ +wGbhZ0f+2GvKcwMmJERnIQjuoNaYSQLT0tM0VB9Iz0rJlZC+tzPZ+5pPqEumRdsS +IzWNXfF42AhcbwAQYQJBAPVXtMYIJc9EZsz86ZcQiMPWUpCX5vnRmtwL8kKyR8D5 +4KfYeiowyFffSRMMcclwNHq7TgSXN+nIXM9WyzyzwikCQQDKbNA28AgZp9aT54HP +WnbeE2pmt+uk/zl/BtxJSoK6H+69Jec+lf7EgL7HgOWYRSNot4uQWu8IhsHLTiUq ++0FtAkEAqwlRxRy4/x24bP+D+QRV0/D97j93joFJbE4Hved7jlSlAV4xDGilwlyv +HNB4Iu5OJ6Gcaibhm+FKkmD3noHSwQJBAIpu3fokLzX0bS+bDFBU6qO3HXX/47xj ++tsfQvkwZrSI8AkU6c8IX0HdVhsz0FBRQAT2ORDQz1XCarfxykNZrwUCQQCGCBIc +BBCWzhHlswlGidWJg3HqqO6hPPClEr3B5G87oCsdeYwiO23XT6rUnoJXfJHp6oCW +5nCwDu5ZTP+khltg +-----END PRIVATE KEY----- diff --git a/integration/util/request/client.go b/integration/util/request/client.go index 771563673e..b35306ce73 100644 --- a/integration/util/request/client.go +++ b/integration/util/request/client.go @@ -1,9 +1,15 @@ package request import ( + "net" + "net/http" "testing" + "time" + "github.com/docker/docker/api" "github.com/docker/docker/client" + "github.com/docker/go-connections/sockets" + "github.com/docker/go-connections/tlsconfig" "github.com/stretchr/testify/require" ) @@ -13,3 +19,35 @@ func NewAPIClient(t *testing.T) client.APIClient { require.NoError(t, err) return clt } + +// NewTLSAPIClient returns a docker API client configured with the +// provided TLS settings +func NewTLSAPIClient(t *testing.T, host, cacertPath, certPath, keyPath string) (client.APIClient, error) { + opts := tlsconfig.Options{ + CAFile: cacertPath, + CertFile: certPath, + KeyFile: keyPath, + ExclusiveRootPools: true, + } + config, err := tlsconfig.Client(opts) + require.Nil(t, err) + tr := &http.Transport{ + TLSClientConfig: config, + DialContext: (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext, + } + proto, addr, _, err := client.ParseHost(host) + require.Nil(t, err) + + sockets.ConfigureTransport(tr, proto, addr) + + httpClient := &http.Client{ + Transport: tr, + CheckRedirect: client.CheckRedirect, + } + verStr := api.DefaultVersion + customHeaders := map[string]string{} + return client.NewClient(host, verStr, httpClient, customHeaders) +} diff --git a/integration/util/requirement/requirement.go b/integration/util/requirement/requirement.go new file mode 100644 index 0000000000..073a9f7c74 --- /dev/null +++ b/integration/util/requirement/requirement.go @@ -0,0 +1,31 @@ +package requirement + +import ( + "net/http" + "strings" + "testing" + "time" + + "github.com/gotestyourself/gotestyourself/skip" +) + +// HasHubConnectivity checks to see if https://hub.docker.com is +// accessible from the present environment +func HasHubConnectivity(t *testing.T) { + // Set a timeout on the GET at 15s + var timeout = 15 * time.Second + var url = "https://hub.docker.com" + + client := http.Client{ + Timeout: timeout, + } + + resp, err := client.Get(url) + if err != nil && strings.Contains(err.Error(), "use of closed network connection") { + t.Fatalf("Timeout for GET request on %s", url) + } + if resp != nil { + resp.Body.Close() + } + skip.IfCondition(t, err != nil) +}