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