github.com/ph/moby@v1.13.1/integration-cli/docker_cli_authz_unix_test.go (about)

     1  // +build !windows
     2  
     3  package main
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"bufio"
    16  	"bytes"
    17  	"os/exec"
    18  	"strconv"
    19  	"time"
    20  
    21  	"net"
    22  	"net/http/httputil"
    23  	"net/url"
    24  
    25  	"github.com/docker/docker/pkg/authorization"
    26  	"github.com/docker/docker/pkg/integration/checker"
    27  	"github.com/docker/docker/pkg/plugins"
    28  	"github.com/go-check/check"
    29  )
    30  
    31  const (
    32  	testAuthZPlugin     = "authzplugin"
    33  	unauthorizedMessage = "User unauthorized authz plugin"
    34  	errorMessage        = "something went wrong..."
    35  	containerListAPI    = "/containers/json"
    36  )
    37  
    38  var (
    39  	alwaysAllowed = []string{"/_ping", "/info"}
    40  )
    41  
    42  func init() {
    43  	check.Suite(&DockerAuthzSuite{
    44  		ds: &DockerSuite{},
    45  	})
    46  }
    47  
    48  type DockerAuthzSuite struct {
    49  	server *httptest.Server
    50  	ds     *DockerSuite
    51  	d      *Daemon
    52  	ctrl   *authorizationController
    53  }
    54  
    55  type authorizationController struct {
    56  	reqRes        authorization.Response // reqRes holds the plugin response to the initial client request
    57  	resRes        authorization.Response // resRes holds the plugin response to the daemon response
    58  	psRequestCnt  int                    // psRequestCnt counts the number of calls to list container request api
    59  	psResponseCnt int                    // psResponseCnt counts the number of calls to list containers response API
    60  	requestsURIs  []string               // requestsURIs stores all request URIs that are sent to the authorization controller
    61  	reqUser       string
    62  	resUser       string
    63  }
    64  
    65  func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
    66  	s.d = NewDaemon(c)
    67  	s.ctrl = &authorizationController{}
    68  }
    69  
    70  func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
    71  	s.d.Stop()
    72  	s.ds.TearDownTest(c)
    73  	s.ctrl = nil
    74  }
    75  
    76  func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
    77  	mux := http.NewServeMux()
    78  	s.server = httptest.NewServer(mux)
    79  
    80  	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
    81  		b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}})
    82  		c.Assert(err, check.IsNil)
    83  		w.Write(b)
    84  	})
    85  
    86  	mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) {
    87  		defer r.Body.Close()
    88  		body, err := ioutil.ReadAll(r.Body)
    89  		c.Assert(err, check.IsNil)
    90  		authReq := authorization.Request{}
    91  		err = json.Unmarshal(body, &authReq)
    92  		c.Assert(err, check.IsNil)
    93  
    94  		assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody)
    95  		assertAuthHeaders(c, authReq.RequestHeaders)
    96  
    97  		// Count only container list api
    98  		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
    99  			s.ctrl.psRequestCnt++
   100  		}
   101  
   102  		s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI)
   103  
   104  		reqRes := s.ctrl.reqRes
   105  		if isAllowed(authReq.RequestURI) {
   106  			reqRes = authorization.Response{Allow: true}
   107  		}
   108  		if reqRes.Err != "" {
   109  			w.WriteHeader(http.StatusInternalServerError)
   110  		}
   111  		b, err := json.Marshal(reqRes)
   112  		c.Assert(err, check.IsNil)
   113  		s.ctrl.reqUser = authReq.User
   114  		w.Write(b)
   115  	})
   116  
   117  	mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) {
   118  		defer r.Body.Close()
   119  		body, err := ioutil.ReadAll(r.Body)
   120  		c.Assert(err, check.IsNil)
   121  		authReq := authorization.Request{}
   122  		err = json.Unmarshal(body, &authReq)
   123  		c.Assert(err, check.IsNil)
   124  
   125  		assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody)
   126  		assertAuthHeaders(c, authReq.ResponseHeaders)
   127  
   128  		// Count only container list api
   129  		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
   130  			s.ctrl.psResponseCnt++
   131  		}
   132  		resRes := s.ctrl.resRes
   133  		if isAllowed(authReq.RequestURI) {
   134  			resRes = authorization.Response{Allow: true}
   135  		}
   136  		if resRes.Err != "" {
   137  			w.WriteHeader(http.StatusInternalServerError)
   138  		}
   139  		b, err := json.Marshal(resRes)
   140  		c.Assert(err, check.IsNil)
   141  		s.ctrl.resUser = authReq.User
   142  		w.Write(b)
   143  	})
   144  
   145  	err := os.MkdirAll("/etc/docker/plugins", 0755)
   146  	c.Assert(err, checker.IsNil)
   147  
   148  	fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
   149  	err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644)
   150  	c.Assert(err, checker.IsNil)
   151  }
   152  
   153  // check for always allowed endpoints to not inhibit test framework functions
   154  func isAllowed(reqURI string) bool {
   155  	for _, endpoint := range alwaysAllowed {
   156  		if strings.HasSuffix(reqURI, endpoint) {
   157  			return true
   158  		}
   159  	}
   160  	return false
   161  }
   162  
   163  // assertAuthHeaders validates authentication headers are removed
   164  func assertAuthHeaders(c *check.C, headers map[string]string) error {
   165  	for k := range headers {
   166  		if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") {
   167  			c.Errorf("Found authentication headers in request '%v'", headers)
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  // assertBody asserts that body is removed for non text/json requests
   174  func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) {
   175  	if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 {
   176  		//return fmt.Errorf("Body included for authentication endpoint %s", string(body))
   177  		c.Errorf("Body included for authentication endpoint %s", string(body))
   178  	}
   179  
   180  	for k, v := range headers {
   181  		if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" {
   182  			return
   183  		}
   184  	}
   185  	if len(body) > 0 {
   186  		c.Errorf("Body included while it should not (Headers: '%v')", headers)
   187  	}
   188  }
   189  
   190  func (s *DockerAuthzSuite) TearDownSuite(c *check.C) {
   191  	if s.server == nil {
   192  		return
   193  	}
   194  
   195  	s.server.Close()
   196  
   197  	err := os.RemoveAll("/etc/docker/plugins")
   198  	c.Assert(err, checker.IsNil)
   199  }
   200  
   201  func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) {
   202  	// start the daemon and load busybox, --net=none build fails otherwise
   203  	// cause it needs to pull busybox
   204  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
   205  	s.ctrl.reqRes.Allow = true
   206  	s.ctrl.resRes.Allow = true
   207  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   208  
   209  	// Ensure command successful
   210  	out, err := s.d.Cmd("run", "-d", "busybox", "top")
   211  	c.Assert(err, check.IsNil)
   212  
   213  	id := strings.TrimSpace(out)
   214  	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
   215  	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id))
   216  
   217  	out, err = s.d.Cmd("ps")
   218  	c.Assert(err, check.IsNil)
   219  	c.Assert(assertContainerList(out, []string{id}), check.Equals, true)
   220  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   221  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   222  }
   223  
   224  func (s *DockerAuthzSuite) TestAuthZPluginTls(c *check.C) {
   225  
   226  	const testDaemonHTTPSAddr = "tcp://localhost:4271"
   227  	// start the daemon and load busybox, --net=none build fails otherwise
   228  	// cause it needs to pull busybox
   229  	if err := s.d.Start(
   230  		"--authorization-plugin="+testAuthZPlugin,
   231  		"--tlsverify",
   232  		"--tlscacert",
   233  		"fixtures/https/ca.pem",
   234  		"--tlscert",
   235  		"fixtures/https/server-cert.pem",
   236  		"--tlskey",
   237  		"fixtures/https/server-key.pem",
   238  		"-H", testDaemonHTTPSAddr); err != nil {
   239  		c.Fatalf("Could not start daemon with busybox: %v", err)
   240  	}
   241  
   242  	s.ctrl.reqRes.Allow = true
   243  	s.ctrl.resRes.Allow = true
   244  
   245  	out, _ := dockerCmd(
   246  		c,
   247  		"--tlsverify",
   248  		"--tlscacert", "fixtures/https/ca.pem",
   249  		"--tlscert", "fixtures/https/client-cert.pem",
   250  		"--tlskey", "fixtures/https/client-key.pem",
   251  		"-H",
   252  		testDaemonHTTPSAddr,
   253  		"version",
   254  	)
   255  	if !strings.Contains(out, "Server") {
   256  		c.Fatalf("docker version should return information of server side")
   257  	}
   258  
   259  	c.Assert(s.ctrl.reqUser, check.Equals, "client")
   260  	c.Assert(s.ctrl.resUser, check.Equals, "client")
   261  }
   262  
   263  func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
   264  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   265  	c.Assert(err, check.IsNil)
   266  	s.ctrl.reqRes.Allow = false
   267  	s.ctrl.reqRes.Msg = unauthorizedMessage
   268  
   269  	// Ensure command is blocked
   270  	res, err := s.d.Cmd("ps")
   271  	c.Assert(err, check.NotNil)
   272  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   273  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 0)
   274  
   275  	// Ensure unauthorized message appears in response
   276  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
   277  }
   278  
   279  // TestAuthZPluginAPIDenyResponse validates that when authorization plugin deny the request, the status code is forbidden
   280  func (s *DockerAuthzSuite) TestAuthZPluginAPIDenyResponse(c *check.C) {
   281  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   282  	c.Assert(err, check.IsNil)
   283  	s.ctrl.reqRes.Allow = false
   284  	s.ctrl.resRes.Msg = unauthorizedMessage
   285  
   286  	daemonURL, err := url.Parse(s.d.sock())
   287  
   288  	conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
   289  	c.Assert(err, check.IsNil)
   290  	client := httputil.NewClientConn(conn, nil)
   291  	req, err := http.NewRequest("GET", "/version", nil)
   292  	c.Assert(err, check.IsNil)
   293  	resp, err := client.Do(req)
   294  
   295  	c.Assert(err, check.IsNil)
   296  	c.Assert(resp.StatusCode, checker.Equals, http.StatusForbidden)
   297  	c.Assert(err, checker.IsNil)
   298  }
   299  
   300  func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
   301  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   302  	c.Assert(err, check.IsNil)
   303  	s.ctrl.reqRes.Allow = true
   304  	s.ctrl.resRes.Allow = false
   305  	s.ctrl.resRes.Msg = unauthorizedMessage
   306  
   307  	// Ensure command is blocked
   308  	res, err := s.d.Cmd("ps")
   309  	c.Assert(err, check.NotNil)
   310  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   311  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   312  
   313  	// Ensure unauthorized message appears in response
   314  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
   315  }
   316  
   317  // TestAuthZPluginAllowEventStream verifies event stream propagates correctly after request pass through by the authorization plugin
   318  func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) {
   319  	testRequires(c, DaemonIsLinux)
   320  
   321  	// start the daemon and load busybox to avoid pulling busybox from Docker Hub
   322  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
   323  	s.ctrl.reqRes.Allow = true
   324  	s.ctrl.resRes.Allow = true
   325  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   326  
   327  	startTime := strconv.FormatInt(daemonTime(c).Unix(), 10)
   328  	// Add another command to to enable event pipelining
   329  	eventsCmd := exec.Command(dockerBinary, "--host", s.d.sock(), "events", "--since", startTime)
   330  	stdout, err := eventsCmd.StdoutPipe()
   331  	if err != nil {
   332  		c.Assert(err, check.IsNil)
   333  	}
   334  
   335  	observer := eventObserver{
   336  		buffer:    new(bytes.Buffer),
   337  		command:   eventsCmd,
   338  		scanner:   bufio.NewScanner(stdout),
   339  		startTime: startTime,
   340  	}
   341  
   342  	err = observer.Start()
   343  	c.Assert(err, checker.IsNil)
   344  	defer observer.Stop()
   345  
   346  	// Create a container and wait for the creation events
   347  	out, err := s.d.Cmd("run", "-d", "busybox", "top")
   348  	c.Assert(err, check.IsNil, check.Commentf(out))
   349  	containerID := strings.TrimSpace(out)
   350  	c.Assert(s.d.waitRun(containerID), checker.IsNil)
   351  
   352  	events := map[string]chan bool{
   353  		"create": make(chan bool, 1),
   354  		"start":  make(chan bool, 1),
   355  	}
   356  
   357  	matcher := matchEventLine(containerID, "container", events)
   358  	processor := processEventMatch(events)
   359  	go observer.Match(matcher, processor)
   360  
   361  	// Ensure all events are received
   362  	for event, eventChannel := range events {
   363  
   364  		select {
   365  		case <-time.After(30 * time.Second):
   366  			// Fail the test
   367  			observer.CheckEventError(c, containerID, event, matcher)
   368  			c.FailNow()
   369  		case <-eventChannel:
   370  			// Ignore, event received
   371  		}
   372  	}
   373  
   374  	// Ensure both events and container endpoints are passed to the authorization plugin
   375  	assertURIRecorded(c, s.ctrl.requestsURIs, "/events")
   376  	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
   377  	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", containerID))
   378  }
   379  
   380  func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) {
   381  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   382  	c.Assert(err, check.IsNil)
   383  	s.ctrl.reqRes.Allow = true
   384  	s.ctrl.resRes.Err = errorMessage
   385  
   386  	// Ensure command is blocked
   387  	res, err := s.d.Cmd("ps")
   388  	c.Assert(err, check.NotNil)
   389  
   390  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage))
   391  }
   392  
   393  func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) {
   394  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   395  	c.Assert(err, check.IsNil)
   396  	s.ctrl.reqRes.Err = errorMessage
   397  
   398  	// Ensure command is blocked
   399  	res, err := s.d.Cmd("ps")
   400  	c.Assert(err, check.NotNil)
   401  
   402  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage))
   403  }
   404  
   405  func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) {
   406  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil)
   407  
   408  	s.ctrl.reqRes.Allow = true
   409  	s.ctrl.resRes.Allow = true
   410  
   411  	out, err := s.d.Cmd("ps")
   412  	c.Assert(err, check.IsNil, check.Commentf(out))
   413  
   414  	// assert plugin is only called once..
   415  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   416  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   417  }
   418  
   419  func (s *DockerAuthzSuite) TestAuthZPluginEnsureLoadImportWorking(c *check.C) {
   420  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil)
   421  	s.ctrl.reqRes.Allow = true
   422  	s.ctrl.resRes.Allow = true
   423  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   424  
   425  	tmp, err := ioutil.TempDir("", "test-authz-load-import")
   426  	c.Assert(err, check.IsNil)
   427  	defer os.RemoveAll(tmp)
   428  
   429  	savedImagePath := filepath.Join(tmp, "save.tar")
   430  
   431  	out, err := s.d.Cmd("save", "-o", savedImagePath, "busybox")
   432  	c.Assert(err, check.IsNil, check.Commentf(out))
   433  	out, err = s.d.Cmd("load", "--input", savedImagePath)
   434  	c.Assert(err, check.IsNil, check.Commentf(out))
   435  
   436  	exportedImagePath := filepath.Join(tmp, "export.tar")
   437  
   438  	out, err = s.d.Cmd("run", "-d", "--name", "testexport", "busybox")
   439  	c.Assert(err, check.IsNil, check.Commentf(out))
   440  	out, err = s.d.Cmd("export", "-o", exportedImagePath, "testexport")
   441  	c.Assert(err, check.IsNil, check.Commentf(out))
   442  	out, err = s.d.Cmd("import", exportedImagePath)
   443  	c.Assert(err, check.IsNil, check.Commentf(out))
   444  }
   445  
   446  func (s *DockerAuthzSuite) TestAuthZPluginHeader(c *check.C) {
   447  	c.Assert(s.d.Start("--debug", "--authorization-plugin="+testAuthZPlugin), check.IsNil)
   448  	s.ctrl.reqRes.Allow = true
   449  	s.ctrl.resRes.Allow = true
   450  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   451  
   452  	daemonURL, err := url.Parse(s.d.sock())
   453  
   454  	conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
   455  	c.Assert(err, check.IsNil)
   456  	client := httputil.NewClientConn(conn, nil)
   457  	req, err := http.NewRequest("GET", "/version", nil)
   458  	c.Assert(err, check.IsNil)
   459  	resp, err := client.Do(req)
   460  
   461  	c.Assert(err, check.IsNil)
   462  	c.Assert(resp.Header["Content-Type"][0], checker.Equals, "application/json")
   463  }
   464  
   465  // assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
   466  func assertURIRecorded(c *check.C, uris []string, uri string) {
   467  	var found bool
   468  	for _, u := range uris {
   469  		if strings.Contains(u, uri) {
   470  			found = true
   471  			break
   472  		}
   473  	}
   474  	if !found {
   475  		c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
   476  	}
   477  }