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