github.com/niusmallnan/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 }