github.com/ojongerius/docker@v1.11.2/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 }