// +build !windows package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "strings" "bufio" "bytes" "os/exec" "strconv" "time" "github.com/docker/docker/pkg/authorization" "github.com/docker/docker/pkg/integration/checker" "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 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 } func (s *DockerAuthzSuite) SetUpTest(c *check.C) { s.d = NewDaemon(c) s.ctrl = &authorizationController{} } func (s *DockerAuthzSuite) TearDownTest(c *check.C) { s.d.Stop() s.ds.TearDownTest(c) s.ctrl = nil } func (s *DockerAuthzSuite) SetUpSuite(c *check.C) { mux := http.NewServeMux() s.server = httptest.NewServer(mux) c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server")) 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) 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) 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 c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true c.Assert(s.d.LoadBusybox(), check.IsNil) // 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)) out, err = s.d.Cmd("ps") c.Assert(err, check.IsNil) c.Assert(assertContainerList(out, []string{id}), check.Equals, true) c.Assert(s.ctrl.psRequestCnt, check.Equals, 1) c.Assert(s.ctrl.psResponseCnt, check.Equals, 1) } func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) { err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) c.Assert(err, check.IsNil) 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)) } func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) { err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) c.Assert(err, check.IsNil) 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 c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil) s.ctrl.reqRes.Allow = true s.ctrl.resRes.Allow = true c.Assert(s.d.LoadBusybox(), check.IsNil) startTime := strconv.FormatInt(daemonTime(c).Unix(), 10) // Add another command to to enable event pipelining eventsCmd := exec.Command(s.d.cmd.Path, "--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) { err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) c.Assert(err, check.IsNil) 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) { err := s.d.Start("--authorization-plugin=" + testAuthZPlugin) c.Assert(err, check.IsNil) 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) { c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil) 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) } // 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, ",")) } }