123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475 |
- // +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) {
- 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))
- 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) 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, ","))
- }
- }
|