github.com/ssdev-go/moby@v17.12.1-ce-rc2+incompatible/integration/plugin/authz/authz_plugin_test.go (about) 1 // +build !windows 2 3 package 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/docker/docker/api/types" 22 "github.com/docker/docker/api/types/container" 23 eventtypes "github.com/docker/docker/api/types/events" 24 networktypes "github.com/docker/docker/api/types/network" 25 "github.com/docker/docker/client" 26 "github.com/docker/docker/integration/util/request" 27 "github.com/docker/docker/internal/test/environment" 28 "github.com/docker/docker/pkg/authorization" 29 "github.com/gotestyourself/gotestyourself/skip" 30 "github.com/stretchr/testify/require" 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 require.Nil(t, err) 61 62 fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin) 63 err = ioutil.WriteFile(fileName, []byte(server.URL), 0644) 64 require.Nil(t, err) 65 66 return func() { 67 err := os.RemoveAll("/etc/docker/plugins") 68 require.Nil(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 client, err := d.NewClient() 92 require.Nil(t, err) 93 94 // Ensure command successful 95 createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{"top"}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "") 96 require.Nil(t, err) 97 98 err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{}) 99 require.Nil(t, err) 100 101 assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") 102 assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", createResponse.ID)) 103 104 _, err = client.ServerVersion(context.Background()) 105 require.Nil(t, err) 106 require.Equal(t, 1, ctrl.versionReqCount) 107 require.Equal(t, 1, ctrl.versionResCount) 108 } 109 110 func TestAuthZPluginTLS(t *testing.T) { 111 defer setupTestV1(t)() 112 const ( 113 testDaemonHTTPSAddr = "tcp://localhost:4271" 114 cacertPath = "../../testdata/https/ca.pem" 115 serverCertPath = "../../testdata/https/server-cert.pem" 116 serverKeyPath = "../../testdata/https/server-key.pem" 117 clientCertPath = "../../testdata/https/client-cert.pem" 118 clientKeyPath = "../../testdata/https/client-key.pem" 119 ) 120 121 d.Start(t, 122 "--authorization-plugin="+testAuthZPlugin, 123 "--tlsverify", 124 "--tlscacert", cacertPath, 125 "--tlscert", serverCertPath, 126 "--tlskey", serverKeyPath, 127 "-H", testDaemonHTTPSAddr) 128 129 ctrl.reqRes.Allow = true 130 ctrl.resRes.Allow = true 131 132 client, err := request.NewTLSAPIClient(t, testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath) 133 require.Nil(t, err) 134 135 _, err = client.ServerVersion(context.Background()) 136 require.Nil(t, err) 137 138 require.Equal(t, "client", ctrl.reqUser) 139 require.Equal(t, "client", ctrl.resUser) 140 } 141 142 func TestAuthZPluginDenyRequest(t *testing.T) { 143 defer setupTestV1(t)() 144 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 145 ctrl.reqRes.Allow = false 146 ctrl.reqRes.Msg = unauthorizedMessage 147 148 client, err := d.NewClient() 149 require.Nil(t, err) 150 151 // Ensure command is blocked 152 _, err = client.ServerVersion(context.Background()) 153 require.NotNil(t, err) 154 require.Equal(t, 1, ctrl.versionReqCount) 155 require.Equal(t, 0, ctrl.versionResCount) 156 157 // Ensure unauthorized message appears in response 158 require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) 159 } 160 161 // TestAuthZPluginAPIDenyResponse validates that when authorization 162 // plugin deny the request, the status code is forbidden 163 func TestAuthZPluginAPIDenyResponse(t *testing.T) { 164 defer setupTestV1(t)() 165 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 166 ctrl.reqRes.Allow = false 167 ctrl.resRes.Msg = unauthorizedMessage 168 169 daemonURL, err := url.Parse(d.Sock()) 170 require.Nil(t, err) 171 172 conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) 173 require.Nil(t, err) 174 client := httputil.NewClientConn(conn, nil) 175 req, err := http.NewRequest("GET", "/version", nil) 176 require.Nil(t, err) 177 resp, err := client.Do(req) 178 179 require.Nil(t, err) 180 require.Equal(t, http.StatusForbidden, resp.StatusCode) 181 } 182 183 func TestAuthZPluginDenyResponse(t *testing.T) { 184 defer setupTestV1(t)() 185 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 186 ctrl.reqRes.Allow = true 187 ctrl.resRes.Allow = false 188 ctrl.resRes.Msg = unauthorizedMessage 189 190 client, err := d.NewClient() 191 require.Nil(t, err) 192 193 // Ensure command is blocked 194 _, err = client.ServerVersion(context.Background()) 195 require.NotNil(t, err) 196 require.Equal(t, 1, ctrl.versionReqCount) 197 require.Equal(t, 1, ctrl.versionResCount) 198 199 // Ensure unauthorized message appears in response 200 require.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error()) 201 } 202 203 // TestAuthZPluginAllowEventStream verifies event stream propagates 204 // correctly after request pass through by the authorization plugin 205 func TestAuthZPluginAllowEventStream(t *testing.T) { 206 skip.IfCondition(t, testEnv.DaemonInfo.OSType != "linux") 207 208 defer setupTestV1(t)() 209 ctrl.reqRes.Allow = true 210 ctrl.resRes.Allow = true 211 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin) 212 213 client, err := d.NewClient() 214 require.Nil(t, err) 215 216 startTime := strconv.FormatInt(systemTime(t, client, testEnv).Unix(), 10) 217 events, errs, cancel := systemEventsSince(client, startTime) 218 defer cancel() 219 220 // Create a container and wait for the creation events 221 createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{"top"}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "") 222 require.Nil(t, err) 223 224 err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{}) 225 require.Nil(t, err) 226 227 for i := 0; i < 100; i++ { 228 c, err := client.ContainerInspect(context.Background(), createResponse.ID) 229 require.Nil(t, err) 230 if c.State.Running { 231 break 232 } 233 if i == 99 { 234 t.Fatal("Container didn't run within 10s") 235 } 236 time.Sleep(100 * time.Millisecond) 237 } 238 239 created := false 240 started := false 241 for !created && !started { 242 select { 243 case event := <-events: 244 if event.Type == eventtypes.ContainerEventType && event.Actor.ID == createResponse.ID { 245 if event.Action == "create" { 246 created = true 247 } 248 if event.Action == "start" { 249 started = true 250 } 251 } 252 case err := <-errs: 253 if err == io.EOF { 254 t.Fatal("premature end of event stream") 255 } 256 require.Nil(t, err) 257 case <-time.After(30 * time.Second): 258 // Fail the test 259 t.Fatal("event stream timeout") 260 } 261 } 262 263 // Ensure both events and container endpoints are passed to the 264 // authorization plugin 265 assertURIRecorded(t, ctrl.requestsURIs, "/events") 266 assertURIRecorded(t, ctrl.requestsURIs, "/containers/create") 267 assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", createResponse.ID)) 268 } 269 270 func systemTime(t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time { 271 if testEnv.IsLocalDaemon() { 272 return time.Now() 273 } 274 275 ctx := context.Background() 276 info, err := client.Info(ctx) 277 require.Nil(t, err) 278 279 dt, err := time.Parse(time.RFC3339Nano, info.SystemTime) 280 require.Nil(t, err, "invalid time format in GET /info response") 281 return dt 282 } 283 284 func systemEventsSince(client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) { 285 eventOptions := types.EventsOptions{ 286 Since: since, 287 } 288 ctx, cancel := context.WithCancel(context.Background()) 289 events, errs := client.Events(ctx, eventOptions) 290 291 return events, errs, cancel 292 } 293 294 func TestAuthZPluginErrorResponse(t *testing.T) { 295 defer setupTestV1(t)() 296 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 297 ctrl.reqRes.Allow = true 298 ctrl.resRes.Err = errorMessage 299 300 client, err := d.NewClient() 301 require.Nil(t, err) 302 303 // Ensure command is blocked 304 _, err = client.ServerVersion(context.Background()) 305 require.NotNil(t, err) 306 require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error()) 307 } 308 309 func TestAuthZPluginErrorRequest(t *testing.T) { 310 defer setupTestV1(t)() 311 d.Start(t, "--authorization-plugin="+testAuthZPlugin) 312 ctrl.reqRes.Err = errorMessage 313 314 client, err := d.NewClient() 315 require.Nil(t, err) 316 317 // Ensure command is blocked 318 _, err = client.ServerVersion(context.Background()) 319 require.NotNil(t, err) 320 require.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error()) 321 } 322 323 func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) { 324 defer setupTestV1(t)() 325 d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) 326 327 ctrl.reqRes.Allow = true 328 ctrl.resRes.Allow = true 329 330 client, err := d.NewClient() 331 require.Nil(t, err) 332 333 _, err = client.ServerVersion(context.Background()) 334 require.Nil(t, err) 335 336 // assert plugin is only called once.. 337 require.Equal(t, 1, ctrl.versionReqCount) 338 require.Equal(t, 1, ctrl.versionResCount) 339 } 340 341 func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) { 342 defer setupTestV1(t)() 343 ctrl.reqRes.Allow = true 344 ctrl.resRes.Allow = true 345 d.StartWithBusybox(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin) 346 347 client, err := d.NewClient() 348 require.Nil(t, err) 349 350 tmp, err := ioutil.TempDir("", "test-authz-load-import") 351 require.Nil(t, err) 352 defer os.RemoveAll(tmp) 353 354 savedImagePath := filepath.Join(tmp, "save.tar") 355 356 err = imageSave(client, savedImagePath, "busybox") 357 require.Nil(t, err) 358 err = imageLoad(client, savedImagePath) 359 require.Nil(t, err) 360 361 exportedImagePath := filepath.Join(tmp, "export.tar") 362 363 createResponse, err := client.ContainerCreate(context.Background(), &container.Config{Cmd: []string{}, Image: "busybox"}, &container.HostConfig{}, &networktypes.NetworkingConfig{}, "") 364 require.Nil(t, err) 365 366 err = client.ContainerStart(context.Background(), createResponse.ID, types.ContainerStartOptions{}) 367 require.Nil(t, err) 368 369 responseReader, err := client.ContainerExport(context.Background(), createResponse.ID) 370 require.Nil(t, err) 371 defer responseReader.Close() 372 file, err := os.Create(exportedImagePath) 373 require.Nil(t, err) 374 defer file.Close() 375 _, err = io.Copy(file, responseReader) 376 require.Nil(t, err) 377 378 err = imageImport(client, exportedImagePath) 379 require.Nil(t, err) 380 } 381 382 func imageSave(client client.APIClient, path, image string) error { 383 ctx := context.Background() 384 responseReader, err := client.ImageSave(ctx, []string{image}) 385 if err != nil { 386 return err 387 } 388 defer responseReader.Close() 389 file, err := os.Create(path) 390 if err != nil { 391 return err 392 } 393 defer file.Close() 394 _, err = io.Copy(file, responseReader) 395 return err 396 } 397 398 func imageLoad(client client.APIClient, path string) error { 399 file, err := os.Open(path) 400 if err != nil { 401 return err 402 } 403 defer file.Close() 404 quiet := true 405 ctx := context.Background() 406 response, err := client.ImageLoad(ctx, file, quiet) 407 if err != nil { 408 return err 409 } 410 defer response.Body.Close() 411 return nil 412 } 413 414 func imageImport(client client.APIClient, path string) error { 415 file, err := os.Open(path) 416 if err != nil { 417 return err 418 } 419 defer file.Close() 420 options := types.ImageImportOptions{} 421 ref := "" 422 source := types.ImageImportSource{ 423 Source: file, 424 SourceName: "-", 425 } 426 ctx := context.Background() 427 responseReader, err := client.ImageImport(ctx, source, ref, options) 428 if err != nil { 429 return err 430 } 431 defer responseReader.Close() 432 return nil 433 } 434 435 func TestAuthZPluginHeader(t *testing.T) { 436 defer setupTestV1(t)() 437 ctrl.reqRes.Allow = true 438 ctrl.resRes.Allow = true 439 d.StartWithBusybox(t, "--debug", "--authorization-plugin="+testAuthZPlugin) 440 441 daemonURL, err := url.Parse(d.Sock()) 442 require.Nil(t, err) 443 444 conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10) 445 require.Nil(t, err) 446 client := httputil.NewClientConn(conn, nil) 447 req, err := http.NewRequest("GET", "/version", nil) 448 require.Nil(t, err) 449 resp, err := client.Do(req) 450 require.Nil(t, err) 451 require.Equal(t, "application/json", resp.Header["Content-Type"][0]) 452 } 453 454 // assertURIRecorded verifies that the given URI was sent and recorded 455 // in the authz plugin 456 func assertURIRecorded(t *testing.T, uris []string, uri string) { 457 var found bool 458 for _, u := range uris { 459 if strings.Contains(u, uri) { 460 found = true 461 break 462 } 463 } 464 if !found { 465 t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ",")) 466 } 467 }