github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/integration/plugin/authz/authz_plugin_test.go (about) 1 // +build !windows 2 3 package authz // import "github.com/demonoid81/moby/integration/plugin/authz" 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net" 11 "net/http" 12 "net/http/httputil" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "testing" 19 "time" 20 21 "github.com/demonoid81/moby/api/types" 22 eventtypes "github.com/demonoid81/moby/api/types/events" 23 "github.com/demonoid81/moby/client" 24 "github.com/demonoid81/moby/integration/internal/container" 25 "github.com/demonoid81/moby/pkg/archive" 26 "github.com/demonoid81/moby/pkg/authorization" 27 "github.com/demonoid81/moby/testutil/environment" 28 "gotest.tools/v3/assert" 29 "gotest.tools/v3/poll" 30 "gotest.tools/v3/skip" 31 ) 32 33 const ( 34 testAuthZPlugin = "authzplugin" 35 unauthorizedMessage = "User unauthorized authz plugin" 36 errorMessage = "something went wrong..." 37 serverVersionAPI = "/version" 38 ) 39 40 var ( 41 alwaysAllowed = []string{"/_ping", "/info"} 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 versionReqCount int // versionReqCount counts the number of requests to the server version API endpoint 49 versionResCount int // versionResCount counts the number of responses from the server version API endpoint 50 requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller 51 reqUser string 52 resUser string 53 } 54 55 func setupTestV1(t *testing.T) func() { 56 ctrl = &authorizationController{} 57 teardown := setupTest(t) 58 59 err := os.MkdirAll("/etc/docker/plugins", 0755) 60 assert.NilError(t, err) 61 62 fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin) 63 err = ioutil.WriteFile(fileName, []byte(server.URL), 0644) 64 assert.NilError(t, err) 65 66 return func() { 67 err := os.RemoveAll("/etc/docker/plugins") 68 assert.NilError(t, err) 69 70 teardown() 71 ctrl = nil 72 } 73 } 74 75 // check for always allowed endpoints to not inhibit test framework functions 76 func isAllowed(reqURI string) bool { 77 for _, endpoint := range alwaysAllowed { 78 if strings.HasSuffix(reqURI, endpoint) { 79 return true 80 } 81 } 82 return false 83 } 84 85 func TestAuthZPluginAllowRequest(t *testing.T) { 86 defer setupTestV1(t)() 87 ctrl.reqRes.Allow = true 88 ctrl.resRes.Allow = true 89 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin) 90 91 c := d.NewClientT(t) 92 ctx := context.Background() 93 94 // Ensure command successful 95 cID := container.Run(ctx, t, c) 96 97 assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") 98 assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID)) 99 100 _, err := c.ServerVersion(ctx) 101 assert.NilError(t, err) 102 assert.Equal(t, 1, ctrl.versionReqCount) 103 assert.Equal(t, 1, ctrl.versionResCount) 104 } 105 106 func TestAuthZPluginTLS(t *testing.T) { 107 defer setupTestV1(t)() 108 const ( 109 testDaemonHTTPSAddr = "tcp://localhost:4271" 110 cacertPath = "../../testdata/https/ca.pem" 111 serverCertPath = "../../testdata/https/server-cert.pem" 112 serverKeyPath = "../../testdata/https/server-key.pem" 113 clientCertPath = "../../testdata/https/client-cert.pem" 114 clientKeyPath = "../../testdata/https/client-key.pem" 115 ) 116 117 d.Start(t, 118 "--authorization-plugin="+testAuthZPlugin, 119 "--tlsverify", 120 "--tlscacert", cacertPath, 121 "--tlscert", serverCertPath, 122 "--tlskey", serverKeyPath, 123 "-H", testDaemonHTTPSAddr) 124 125 ctrl.reqRes.Allow = true 126 ctrl.resRes.Allow = true 127 128 c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath) 129 assert.NilError(t, err) 130 131 _, err = c.ServerVersion(context.Background()) 132 assert.NilError(t, err) 133 134 assert.Equal(t, "client", ctrl.reqUser) 135 assert.Equal(t, "client", ctrl.resUser) 136 } 137 138 func newTLSAPIClient(host, cacertPath, certPath, keyPath string) (client.APIClient, error) { 139 dialer := &net.Dialer{ 140 KeepAlive: 30 * time.Second, 141 Timeout: 30 * time.Second, 142 } 143 return client.NewClientWithOpts( 144 client.WithTLSClientConfig(cacertPath, certPath, keyPath), 145 client.WithDialContext(dialer.DialContext), 146 client.WithHost(host)) 147 } 148 149 func TestAuthZPluginDenyRequest(t *testing.T) { 150 defer setupTestV1(t)() 151 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 152 ctrl.reqRes.Allow = false 153 ctrl.reqRes.Msg = unauthorizedMessage 154 155 c := d.NewClientT(t) 156 157 // Ensure command is blocked 158 _, err := c.ServerVersion(context.Background()) 159 assert.Assert(t, err != nil) 160 assert.Equal(t, 1, ctrl.versionReqCount) 161 assert.Equal(t, 0, ctrl.versionResCount) 162 163 // Ensure unauthorized message appears in response 164 assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) 165 } 166 167 // TestAuthZPluginAPIDenyResponse validates that when authorization 168 // plugin deny the request, the status code is forbidden 169 func TestAuthZPluginAPIDenyResponse(t *testing.T) { 170 defer setupTestV1(t)() 171 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 172 ctrl.reqRes.Allow = false 173 ctrl.resRes.Msg = unauthorizedMessage 174 175 daemonURL, err := url.Parse(d.Sock()) 176 assert.NilError(t, err) 177 178 conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) 179 assert.NilError(t, err) 180 c := httputil.NewClientConn(conn, nil) 181 req, err := http.NewRequest(http.MethodGet, "/version", nil) 182 assert.NilError(t, err) 183 resp, err := c.Do(req) 184 185 assert.NilError(t, err) 186 assert.DeepEqual(t, http.StatusForbidden, resp.StatusCode) 187 } 188 189 func TestAuthZPluginDenyResponse(t *testing.T) { 190 defer setupTestV1(t)() 191 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 192 ctrl.reqRes.Allow = true 193 ctrl.resRes.Allow = false 194 ctrl.resRes.Msg = unauthorizedMessage 195 196 c := d.NewClientT(t) 197 198 // Ensure command is blocked 199 _, err := c.ServerVersion(context.Background()) 200 assert.Assert(t, err != nil) 201 assert.Equal(t, 1, ctrl.versionReqCount) 202 assert.Equal(t, 1, ctrl.versionResCount) 203 204 // Ensure unauthorized message appears in response 205 assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) 206 } 207 208 // TestAuthZPluginAllowEventStream verifies event stream propagates 209 // correctly after request pass through by the authorization plugin 210 func TestAuthZPluginAllowEventStream(t *testing.T) { 211 skip.If(t, testEnv.DaemonInfo.OSType != "linux") 212 skip.If(t, testEnv.DaemonInfo.OSType == "windows") 213 214 defer setupTestV1(t)() 215 ctrl.reqRes.Allow = true 216 ctrl.resRes.Allow = true 217 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin) 218 219 c := d.NewClientT(t) 220 ctx := context.Background() 221 222 startTime := strconv.FormatInt(systemTime(t, c, testEnv).Unix(), 10) 223 events, errs, cancel := systemEventsSince(c, startTime) 224 defer cancel() 225 226 // Create a container and wait for the creation events 227 cID := container.Run(ctx, t, c) 228 poll.WaitOn(t, container.IsInState(ctx, c, cID, "running")) 229 230 created := false 231 started := false 232 for !created && !started { 233 select { 234 case event := <-events: 235 if event.Type == eventtypes.ContainerEventType && event.Actor.ID == cID { 236 if event.Action == "create" { 237 created = true 238 } 239 if event.Action == "start" { 240 started = true 241 } 242 } 243 case err := <-errs: 244 if err == io.EOF { 245 t.Fatal("premature end of event stream") 246 } 247 assert.NilError(t, err) 248 case <-time.After(30 * time.Second): 249 // Fail the test 250 t.Fatal("event stream timeout") 251 } 252 } 253 254 // Ensure both events and container endpoints are passed to the 255 // authorization plugin 256 assertURIRecorded(t, ctrl.requestsURIs, "/events") 257 assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") 258 assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID)) 259 } 260 261 func systemTime(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time { 262 if testEnv.IsLocalDaemon() { 263 return time.Now() 264 } 265 266 ctx := context.Background() 267 info, err := client.Info(ctx) 268 assert.NilError(t, err) 269 270 dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) 271 assert.NilError(t, err, "invalid time format in GET /info response") 272 return dt 273 } 274 275 func systemEventsSince(client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) { 276 eventOptions := types.EventsOptions{ 277 Since: since, 278 } 279 ctx, cancel := context.WithCancel(context.Background()) 280 events, errs := client.Events(ctx, eventOptions) 281 282 return events, errs, cancel 283 } 284 285 func TestAuthZPluginErrorResponse(t *testing.T) { 286 defer setupTestV1(t)() 287 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 288 ctrl.reqRes.Allow = true 289 ctrl.resRes.Err = errorMessage 290 291 c := d.NewClientT(t) 292 293 // Ensure command is blocked 294 _, err := c.ServerVersion(context.Background()) 295 assert.Assert(t, err != nil) 296 assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error()) 297 } 298 299 func TestAuthZPluginErrorRequest(t *testing.T) { 300 defer setupTestV1(t)() 301 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 302 ctrl.reqRes.Err = errorMessage 303 304 c := d.NewClientT(t) 305 306 // Ensure command is blocked 307 _, err := c.ServerVersion(context.Background()) 308 assert.Assert(t, err != nil) 309 assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error()) 310 } 311 312 func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) { 313 defer setupTestV1(t)() 314 d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) 315 316 ctrl.reqRes.Allow = true 317 ctrl.resRes.Allow = true 318 319 c := d.NewClientT(t) 320 321 _, err := c.ServerVersion(context.Background()) 322 assert.NilError(t, err) 323 324 // assert plugin is only called once.. 325 assert.Equal(t, 1, ctrl.versionReqCount) 326 assert.Equal(t, 1, ctrl.versionResCount) 327 } 328 329 func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) { 330 defer setupTestV1(t)() 331 ctrl.reqRes.Allow = true 332 ctrl.resRes.Allow = true 333 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) 334 335 c := d.NewClientT(t) 336 ctx := context.Background() 337 338 tmp, err := ioutil.TempDir("", "test-authz-load-import") 339 assert.NilError(t, err) 340 defer os.RemoveAll(tmp) 341 342 savedImagePath := filepath.Join(tmp, "save.tar") 343 344 err = imageSave(c, savedImagePath, "busybox") 345 assert.NilError(t, err) 346 err = imageLoad(c, savedImagePath) 347 assert.NilError(t, err) 348 349 exportedImagePath := filepath.Join(tmp, "export.tar") 350 351 cID := container.Run(ctx, t, c) 352 353 responseReader, err := c.ContainerExport(context.Background(), cID) 354 assert.NilError(t, err) 355 defer responseReader.Close() 356 file, err := os.Create(exportedImagePath) 357 assert.NilError(t, err) 358 defer file.Close() 359 _, err = io.Copy(file, responseReader) 360 assert.NilError(t, err) 361 362 err = imageImport(c, exportedImagePath) 363 assert.NilError(t, err) 364 } 365 366 func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) { 367 defer setupTestV1(t)() 368 ctrl.reqRes.Allow = true 369 ctrl.resRes.Allow = true 370 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) 371 372 dir, err := ioutil.TempDir("", t.Name()) 373 assert.NilError(t, err) 374 defer os.RemoveAll(dir) 375 376 f, err := ioutil.TempFile(dir, "send") 377 assert.NilError(t, err) 378 defer f.Close() 379 380 buf := make([]byte, 1024) 381 fileSize := len(buf) * 1024 * 10 382 for written := 0; written < fileSize; { 383 n, err := f.Write(buf) 384 assert.NilError(t, err) 385 written += n 386 } 387 388 c := d.NewClientT(t) 389 ctx := context.Background() 390 391 cID := container.Run(ctx, t, c) 392 defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) 393 394 _, err = f.Seek(0, io.SeekStart) 395 assert.NilError(t, err) 396 397 srcInfo, err := archive.CopyInfoSourcePath(f.Name(), false) 398 assert.NilError(t, err) 399 srcArchive, err := archive.TarResource(srcInfo) 400 assert.NilError(t, err) 401 defer srcArchive.Close() 402 403 dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"}) 404 assert.NilError(t, err) 405 406 err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, types.CopyToContainerOptions{}) 407 assert.NilError(t, err) 408 409 rdr, _, err := c.CopyFromContainer(ctx, cID, "/test") 410 assert.NilError(t, err) 411 _, err = io.Copy(ioutil.Discard, rdr) 412 assert.NilError(t, err) 413 } 414 415 func imageSave(client client.APIClient, path, image string) error { 416 ctx := context.Background() 417 responseReader, err := client.ImageSave(ctx, []string{image}) 418 if err != nil { 419 return err 420 } 421 defer responseReader.Close() 422 file, err := os.Create(path) 423 if err != nil { 424 return err 425 } 426 defer file.Close() 427 _, err = io.Copy(file, responseReader) 428 return err 429 } 430 431 func imageLoad(client client.APIClient, path string) error { 432 file, err := os.Open(path) 433 if err != nil { 434 return err 435 } 436 defer file.Close() 437 quiet := true 438 ctx := context.Background() 439 response, err := client.ImageLoad(ctx, file, quiet) 440 if err != nil { 441 return err 442 } 443 defer response.Body.Close() 444 return nil 445 } 446 447 func imageImport(client client.APIClient, path string) error { 448 file, err := os.Open(path) 449 if err != nil { 450 return err 451 } 452 defer file.Close() 453 options := types.ImageImportOptions{} 454 ref := "" 455 source := types.ImageImportSource{ 456 Source: file, 457 SourceName: "-", 458 } 459 ctx := context.Background() 460 responseReader, err := client.ImageImport(ctx, source, ref, options) 461 if err != nil { 462 return err 463 } 464 defer responseReader.Close() 465 return nil 466 } 467 468 func TestAuthZPluginHeader(t *testing.T) { 469 defer setupTestV1(t)() 470 ctrl.reqRes.Allow = true 471 ctrl.resRes.Allow = true 472 d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin) 473 474 daemonURL, err := url.Parse(d.Sock()) 475 assert.NilError(t, err) 476 477 conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) 478 assert.NilError(t, err) 479 client := httputil.NewClientConn(conn, nil) 480 req, err := http.NewRequest(http.MethodGet, "/version", nil) 481 assert.NilError(t, err) 482 resp, err := client.Do(req) 483 assert.NilError(t, err) 484 assert.Equal(t, "application/json", resp.Header["Content-Type"][0]) 485 } 486 487 // assertURIRecorded verifies that the given URI was sent and recorded 488 // in the authz plugin 489 func assertURIRecorded(t *testing.T, uris []string, uri string) { 490 var found bool 491 for _, u := range uris { 492 if strings.Contains(u, uri) { 493 found = true 494 break 495 } 496 } 497 if !found { 498 t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ",")) 499 } 500 }