github.com/akerouanton/docker@v1.11.0-rc3/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  	"strings"
    13  
    14  	"bufio"
    15  	"bytes"
    16  	"os/exec"
    17  	"strconv"
    18  	"time"
    19  
    20  	"github.com/docker/docker/pkg/authorization"
    21  	"github.com/docker/docker/pkg/integration/checker"
    22  	"github.com/docker/docker/pkg/plugins"
    23  	"github.com/go-check/check"
    24  )
    25  
    26  const (
    27  	testAuthZPlugin     = "authzplugin"
    28  	unauthorizedMessage = "User unauthorized authz plugin"
    29  	errorMessage        = "something went wrong..."
    30  	containerListAPI    = "/containers/json"
    31  )
    32  
    33  var (
    34  	alwaysAllowed = []string{"/_ping", "/info"}
    35  )
    36  
    37  func init() {
    38  	check.Suite(&DockerAuthzSuite{
    39  		ds: &DockerSuite{},
    40  	})
    41  }
    42  
    43  type DockerAuthzSuite struct {
    44  	server *httptest.Server
    45  	ds     *DockerSuite
    46  	d      *Daemon
    47  	ctrl   *authorizationController
    48  }
    49  
    50  type authorizationController struct {
    51  	reqRes        authorization.Response // reqRes holds the plugin response to the initial client request
    52  	resRes        authorization.Response // resRes holds the plugin response to the daemon response
    53  	psRequestCnt  int                    // psRequestCnt counts the number of calls to list container request api
    54  	psResponseCnt int                    // psResponseCnt counts the number of calls to list containers response API
    55  	requestsURIs  []string               // requestsURIs stores all request URIs that are sent to the authorization controller
    56  }
    57  
    58  func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
    59  	s.d = NewDaemon(c)
    60  	s.ctrl = &authorizationController{}
    61  }
    62  
    63  func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
    64  	s.d.Stop()
    65  	s.ds.TearDownTest(c)
    66  	s.ctrl = nil
    67  }
    68  
    69  func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
    70  	mux := http.NewServeMux()
    71  	s.server = httptest.NewServer(mux)
    72  	c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server"))
    73  
    74  	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
    75  		b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}})
    76  		c.Assert(err, check.IsNil)
    77  		w.Write(b)
    78  	})
    79  
    80  	mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) {
    81  		defer r.Body.Close()
    82  		body, err := ioutil.ReadAll(r.Body)
    83  		c.Assert(err, check.IsNil)
    84  		authReq := authorization.Request{}
    85  		err = json.Unmarshal(body, &authReq)
    86  		c.Assert(err, check.IsNil)
    87  
    88  		assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody)
    89  		assertAuthHeaders(c, authReq.RequestHeaders)
    90  
    91  		// Count only container list api
    92  		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
    93  			s.ctrl.psRequestCnt++
    94  		}
    95  
    96  		s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI)
    97  
    98  		reqRes := s.ctrl.reqRes
    99  		if isAllowed(authReq.RequestURI) {
   100  			reqRes = authorization.Response{Allow: true}
   101  		}
   102  		if reqRes.Err != "" {
   103  			w.WriteHeader(http.StatusInternalServerError)
   104  		}
   105  		b, err := json.Marshal(reqRes)
   106  		c.Assert(err, check.IsNil)
   107  		w.Write(b)
   108  	})
   109  
   110  	mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) {
   111  		defer r.Body.Close()
   112  		body, err := ioutil.ReadAll(r.Body)
   113  		c.Assert(err, check.IsNil)
   114  		authReq := authorization.Request{}
   115  		err = json.Unmarshal(body, &authReq)
   116  		c.Assert(err, check.IsNil)
   117  
   118  		assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody)
   119  		assertAuthHeaders(c, authReq.ResponseHeaders)
   120  
   121  		// Count only container list api
   122  		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
   123  			s.ctrl.psResponseCnt++
   124  		}
   125  		resRes := s.ctrl.resRes
   126  		if isAllowed(authReq.RequestURI) {
   127  			resRes = authorization.Response{Allow: true}
   128  		}
   129  		if resRes.Err != "" {
   130  			w.WriteHeader(http.StatusInternalServerError)
   131  		}
   132  		b, err := json.Marshal(resRes)
   133  		c.Assert(err, check.IsNil)
   134  		w.Write(b)
   135  	})
   136  
   137  	err := os.MkdirAll("/etc/docker/plugins", 0755)
   138  	c.Assert(err, checker.IsNil)
   139  
   140  	fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
   141  	err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644)
   142  	c.Assert(err, checker.IsNil)
   143  }
   144  
   145  // check for always allowed endpoints to not inhibit test framework functions
   146  func isAllowed(reqURI string) bool {
   147  	for _, endpoint := range alwaysAllowed {
   148  		if strings.HasSuffix(reqURI, endpoint) {
   149  			return true
   150  		}
   151  	}
   152  	return false
   153  }
   154  
   155  // assertAuthHeaders validates authentication headers are removed
   156  func assertAuthHeaders(c *check.C, headers map[string]string) error {
   157  	for k := range headers {
   158  		if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") {
   159  			c.Errorf("Found authentication headers in request '%v'", headers)
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  // assertBody asserts that body is removed for non text/json requests
   166  func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) {
   167  	if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 {
   168  		//return fmt.Errorf("Body included for authentication endpoint %s", string(body))
   169  		c.Errorf("Body included for authentication endpoint %s", string(body))
   170  	}
   171  
   172  	for k, v := range headers {
   173  		if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" {
   174  			return
   175  		}
   176  	}
   177  	if len(body) > 0 {
   178  		c.Errorf("Body included while it should not (Headers: '%v')", headers)
   179  	}
   180  }
   181  
   182  func (s *DockerAuthzSuite) TearDownSuite(c *check.C) {
   183  	if s.server == nil {
   184  		return
   185  	}
   186  
   187  	s.server.Close()
   188  
   189  	err := os.RemoveAll("/etc/docker/plugins")
   190  	c.Assert(err, checker.IsNil)
   191  }
   192  
   193  func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) {
   194  	// start the daemon and load busybox, --net=none build fails otherwise
   195  	// cause it needs to pull busybox
   196  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
   197  	s.ctrl.reqRes.Allow = true
   198  	s.ctrl.resRes.Allow = true
   199  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   200  
   201  	// Ensure command successful
   202  	out, err := s.d.Cmd("run", "-d", "busybox", "top")
   203  	c.Assert(err, check.IsNil)
   204  
   205  	id := strings.TrimSpace(out)
   206  	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
   207  	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id))
   208  
   209  	out, err = s.d.Cmd("ps")
   210  	c.Assert(err, check.IsNil)
   211  	c.Assert(assertContainerList(out, []string{id}), check.Equals, true)
   212  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   213  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   214  }
   215  
   216  func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
   217  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   218  	c.Assert(err, check.IsNil)
   219  	s.ctrl.reqRes.Allow = false
   220  	s.ctrl.reqRes.Msg = unauthorizedMessage
   221  
   222  	// Ensure command is blocked
   223  	res, err := s.d.Cmd("ps")
   224  	c.Assert(err, check.NotNil)
   225  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   226  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 0)
   227  
   228  	// Ensure unauthorized message appears in response
   229  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
   230  }
   231  
   232  func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
   233  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   234  	c.Assert(err, check.IsNil)
   235  	s.ctrl.reqRes.Allow = true
   236  	s.ctrl.resRes.Allow = false
   237  	s.ctrl.resRes.Msg = unauthorizedMessage
   238  
   239  	// Ensure command is blocked
   240  	res, err := s.d.Cmd("ps")
   241  	c.Assert(err, check.NotNil)
   242  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   243  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   244  
   245  	// Ensure unauthorized message appears in response
   246  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
   247  }
   248  
   249  // TestAuthZPluginAllowEventStream verifies event stream propagates correctly after request pass through by the authorization plugin
   250  func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) {
   251  	testRequires(c, DaemonIsLinux)
   252  
   253  	// start the daemon and load busybox to avoid pulling busybox from Docker Hub
   254  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
   255  	s.ctrl.reqRes.Allow = true
   256  	s.ctrl.resRes.Allow = true
   257  	c.Assert(s.d.LoadBusybox(), check.IsNil)
   258  
   259  	startTime := strconv.FormatInt(daemonTime(c).Unix(), 10)
   260  	// Add another command to to enable event pipelining
   261  	eventsCmd := exec.Command(s.d.cmd.Path, "--host", s.d.sock(), "events", "--since", startTime)
   262  	stdout, err := eventsCmd.StdoutPipe()
   263  	if err != nil {
   264  		c.Assert(err, check.IsNil)
   265  	}
   266  
   267  	observer := eventObserver{
   268  		buffer:    new(bytes.Buffer),
   269  		command:   eventsCmd,
   270  		scanner:   bufio.NewScanner(stdout),
   271  		startTime: startTime,
   272  	}
   273  
   274  	err = observer.Start()
   275  	c.Assert(err, checker.IsNil)
   276  	defer observer.Stop()
   277  
   278  	// Create a container and wait for the creation events
   279  	out, err := s.d.Cmd("run", "-d", "busybox", "top")
   280  	c.Assert(err, check.IsNil, check.Commentf(out))
   281  	containerID := strings.TrimSpace(out)
   282  	c.Assert(s.d.waitRun(containerID), checker.IsNil)
   283  
   284  	events := map[string]chan bool{
   285  		"create": make(chan bool, 1),
   286  		"start":  make(chan bool, 1),
   287  	}
   288  
   289  	matcher := matchEventLine(containerID, "container", events)
   290  	processor := processEventMatch(events)
   291  	go observer.Match(matcher, processor)
   292  
   293  	// Ensure all events are received
   294  	for event, eventChannel := range events {
   295  
   296  		select {
   297  		case <-time.After(30 * time.Second):
   298  			// Fail the test
   299  			observer.CheckEventError(c, containerID, event, matcher)
   300  			c.FailNow()
   301  		case <-eventChannel:
   302  			// Ignore, event received
   303  		}
   304  	}
   305  
   306  	// Ensure both events and container endpoints are passed to the authorization plugin
   307  	assertURIRecorded(c, s.ctrl.requestsURIs, "/events")
   308  	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
   309  	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", containerID))
   310  }
   311  
   312  func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) {
   313  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   314  	c.Assert(err, check.IsNil)
   315  	s.ctrl.reqRes.Allow = true
   316  	s.ctrl.resRes.Err = errorMessage
   317  
   318  	// Ensure command is blocked
   319  	res, err := s.d.Cmd("ps")
   320  	c.Assert(err, check.NotNil)
   321  
   322  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage))
   323  }
   324  
   325  func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) {
   326  	err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
   327  	c.Assert(err, check.IsNil)
   328  	s.ctrl.reqRes.Err = errorMessage
   329  
   330  	// Ensure command is blocked
   331  	res, err := s.d.Cmd("ps")
   332  	c.Assert(err, check.NotNil)
   333  
   334  	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage))
   335  }
   336  
   337  func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) {
   338  	c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil)
   339  
   340  	s.ctrl.reqRes.Allow = true
   341  	s.ctrl.resRes.Allow = true
   342  
   343  	out, err := s.d.Cmd("ps")
   344  	c.Assert(err, check.IsNil, check.Commentf(out))
   345  
   346  	// assert plugin is only called once..
   347  	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
   348  	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
   349  }
   350  
   351  // assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
   352  func assertURIRecorded(c *check.C, uris []string, uri string) {
   353  	var found bool
   354  	for _, u := range uris {
   355  		if strings.Contains(u, uri) {
   356  			found = true
   357  			break
   358  		}
   359  	}
   360  	if !found {
   361  		c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
   362  	}
   363  }