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