123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- //go:build !windows
- package authz // import "github.com/docker/docker/integration/plugin/authz"
- import (
- "context"
- "fmt"
- "io"
- "net"
- "net/http"
- "net/http/httputil"
- "net/url"
- "os"
- "path/filepath"
- "strconv"
- "strings"
- "testing"
- "time"
- "github.com/docker/docker/api/types"
- containertypes "github.com/docker/docker/api/types/container"
- eventtypes "github.com/docker/docker/api/types/events"
- "github.com/docker/docker/client"
- "github.com/docker/docker/integration/internal/container"
- "github.com/docker/docker/pkg/archive"
- "github.com/docker/docker/pkg/authorization"
- "github.com/docker/docker/testutil/environment"
- "gotest.tools/v3/assert"
- "gotest.tools/v3/skip"
- )
- const (
- testAuthZPlugin = "authzplugin"
- unauthorizedMessage = "User unauthorized authz plugin"
- errorMessage = "something went wrong..."
- serverVersionAPI = "/version"
- )
- var (
- alwaysAllowed = []string{"/_ping", "/info"}
- ctrl *authorizationController
- )
- type authorizationController struct {
- reqRes authorization.Response // reqRes holds the plugin response to the initial client request
- resRes authorization.Response // resRes holds the plugin response to the daemon response
- versionReqCount int // versionReqCount counts the number of requests to the server version API endpoint
- versionResCount int // versionResCount counts the number of responses from the server version API endpoint
- requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller
- reqUser string
- resUser string
- }
- func setupTestV1(t *testing.T) context.Context {
- ctx := setupTest(t)
- ctrl = &authorizationController{}
- err := os.MkdirAll("/etc/docker/plugins", 0o755)
- assert.NilError(t, err)
- fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
- err = os.WriteFile(fileName, []byte(server.URL), 0o644)
- assert.NilError(t, err)
- t.Cleanup(func() {
- err := os.RemoveAll("/etc/docker/plugins")
- assert.NilError(t, err)
- ctrl = nil
- })
- return ctx
- }
- // check for always allowed endpoints to not inhibit test framework functions
- func isAllowed(reqURI string) bool {
- for _, endpoint := range alwaysAllowed {
- if strings.HasSuffix(reqURI, endpoint) {
- return true
- }
- }
- return false
- }
- func TestAuthZPluginAllowRequest(t *testing.T) {
- ctx := setupTestV1(t)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin)
- c := d.NewClientT(t)
- // Ensure command successful
- cID := container.Run(ctx, t, c)
- assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
- assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
- _, err := c.ServerVersion(ctx)
- assert.NilError(t, err)
- assert.Equal(t, 1, ctrl.versionReqCount)
- assert.Equal(t, 1, ctrl.versionResCount)
- }
- func TestAuthZPluginTLS(t *testing.T) {
- ctx := setupTestV1(t)
- const (
- testDaemonHTTPSAddr = "tcp://localhost:4271"
- cacertPath = "../../testdata/https/ca.pem"
- serverCertPath = "../../testdata/https/server-cert.pem"
- serverKeyPath = "../../testdata/https/server-key.pem"
- clientCertPath = "../../testdata/https/client-cert.pem"
- clientKeyPath = "../../testdata/https/client-key.pem"
- )
- d.Start(t,
- "--authorization-plugin="+testAuthZPlugin,
- "--tlsverify",
- "--tlscacert", cacertPath,
- "--tlscert", serverCertPath,
- "--tlskey", serverKeyPath,
- "-H", testDaemonHTTPSAddr)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- c, err := newTLSAPIClient(testDaemonHTTPSAddr, cacertPath, clientCertPath, clientKeyPath)
- assert.NilError(t, err)
- _, err = c.ServerVersion(ctx)
- assert.NilError(t, err)
- assert.Equal(t, "client", ctrl.reqUser)
- assert.Equal(t, "client", ctrl.resUser)
- }
- func newTLSAPIClient(host, cacertPath, certPath, keyPath string) (client.APIClient, error) {
- dialer := &net.Dialer{
- KeepAlive: 30 * time.Second,
- Timeout: 30 * time.Second,
- }
- return client.NewClientWithOpts(
- client.WithTLSClientConfig(cacertPath, certPath, keyPath),
- client.WithDialContext(dialer.DialContext),
- client.WithHost(host))
- }
- func TestAuthZPluginDenyRequest(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Allow = false
- ctrl.reqRes.Msg = unauthorizedMessage
- c := d.NewClientT(t)
- // Ensure command is blocked
- _, err := c.ServerVersion(ctx)
- assert.Assert(t, err != nil)
- assert.Equal(t, 1, ctrl.versionReqCount)
- assert.Equal(t, 0, ctrl.versionResCount)
- // Ensure unauthorized message appears in response
- assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
- }
- // TestAuthZPluginAPIDenyResponse validates that when authorization
- // plugin deny the request, the status code is forbidden
- func TestAuthZPluginAPIDenyResponse(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Allow = false
- ctrl.resRes.Msg = unauthorizedMessage
- daemonURL, err := url.Parse(d.Sock())
- assert.NilError(t, err)
- conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
- assert.NilError(t, err)
- c := httputil.NewClientConn(conn, nil)
- req, err := http.NewRequest(http.MethodGet, "/version", nil)
- assert.NilError(t, err)
- req = req.WithContext(ctx)
- resp, err := c.Do(req)
- assert.NilError(t, err)
- assert.DeepEqual(t, http.StatusForbidden, resp.StatusCode)
- }
- func TestAuthZPluginDenyResponse(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = false
- ctrl.resRes.Msg = unauthorizedMessage
- c := d.NewClientT(t)
- // Ensure command is blocked
- _, err := c.ServerVersion(ctx)
- assert.Assert(t, err != nil)
- assert.Equal(t, 1, ctrl.versionReqCount)
- assert.Equal(t, 1, ctrl.versionResCount)
- // Ensure unauthorized message appears in response
- assert.Equal(t, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s", testAuthZPlugin, unauthorizedMessage), err.Error())
- }
- // TestAuthZPluginAllowEventStream verifies event stream propagates
- // correctly after request pass through by the authorization plugin
- func TestAuthZPluginAllowEventStream(t *testing.T) {
- skip.If(t, testEnv.DaemonInfo.OSType != "linux")
- skip.If(t, testEnv.DaemonInfo.OSType == "windows")
- ctx := setupTestV1(t)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin)
- c := d.NewClientT(t)
- startTime := strconv.FormatInt(systemTime(ctx, t, c, testEnv).Unix(), 10)
- events, errs, cancel := systemEventsSince(ctx, c, startTime)
- defer cancel()
- // Create a container and wait for the creation events
- cID := container.Run(ctx, t, c)
- created := false
- started := false
- for !created && !started {
- select {
- case event := <-events:
- if event.Type == eventtypes.ContainerEventType && event.Actor.ID == cID {
- if event.Action == eventtypes.ActionCreate {
- created = true
- }
- if event.Action == eventtypes.ActionStart {
- started = true
- }
- }
- case err := <-errs:
- if err == io.EOF {
- t.Fatal("premature end of event stream")
- }
- assert.NilError(t, err)
- case <-time.After(30 * time.Second):
- // Fail the test
- t.Fatal("event stream timeout")
- }
- }
- // Ensure both events and container endpoints are passed to the
- // authorization plugin
- assertURIRecorded(t, ctrl.requestsURIs, "/events")
- assertURIRecorded(t, ctrl.requestsURIs, "/containers/create")
- assertURIRecorded(t, ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", cID))
- }
- func systemTime(ctx context.Context, t *testing.T, client client.APIClient, testEnv *environment.Execution) time.Time {
- if testEnv.IsLocalDaemon() {
- return time.Now()
- }
- info, err := client.Info(ctx)
- assert.NilError(t, err)
- dt, err := time.Parse(time.RFC3339Nano, info.SystemTime)
- assert.NilError(t, err, "invalid time format in GET /info response")
- return dt
- }
- func systemEventsSince(ctx context.Context, client client.APIClient, since string) (<-chan eventtypes.Message, <-chan error, func()) {
- eventOptions := types.EventsOptions{
- Since: since,
- }
- ctx, cancel := context.WithCancel(ctx)
- events, errs := client.Events(ctx, eventOptions)
- return events, errs, cancel
- }
- func TestAuthZPluginErrorResponse(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Err = errorMessage
- c := d.NewClientT(t)
- // Ensure command is blocked
- _, err := c.ServerVersion(ctx)
- assert.Assert(t, err != nil)
- assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage), err.Error())
- }
- func TestAuthZPluginErrorRequest(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Err = errorMessage
- c := d.NewClientT(t)
- // Ensure command is blocked
- _, err := c.ServerVersion(ctx)
- assert.Assert(t, err != nil)
- assert.Equal(t, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage), err.Error())
- }
- func TestAuthZPluginEnsureNoDuplicatePluginRegistration(t *testing.T) {
- ctx := setupTestV1(t)
- d.Start(t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- c := d.NewClientT(t)
- _, err := c.ServerVersion(ctx)
- assert.NilError(t, err)
- // assert plugin is only called once..
- assert.Equal(t, 1, ctrl.versionReqCount)
- assert.Equal(t, 1, ctrl.versionResCount)
- }
- func TestAuthZPluginEnsureLoadImportWorking(t *testing.T) {
- ctx := setupTestV1(t)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
- c := d.NewClientT(t)
- tmp, err := os.MkdirTemp("", "test-authz-load-import")
- assert.NilError(t, err)
- defer os.RemoveAll(tmp)
- savedImagePath := filepath.Join(tmp, "save.tar")
- err = imageSave(ctx, c, savedImagePath, "busybox")
- assert.NilError(t, err)
- err = imageLoad(ctx, c, savedImagePath)
- assert.NilError(t, err)
- exportedImagePath := filepath.Join(tmp, "export.tar")
- cID := container.Run(ctx, t, c)
- responseReader, err := c.ContainerExport(ctx, cID)
- assert.NilError(t, err)
- defer responseReader.Close()
- file, err := os.Create(exportedImagePath)
- assert.NilError(t, err)
- defer file.Close()
- _, err = io.Copy(file, responseReader)
- assert.NilError(t, err)
- err = imageImport(ctx, c, exportedImagePath)
- assert.NilError(t, err)
- }
- func TestAuthzPluginEnsureContainerCopyToFrom(t *testing.T) {
- ctx := setupTestV1(t)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- d.StartWithBusybox(ctx, t, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
- dir, err := os.MkdirTemp("", t.Name())
- assert.NilError(t, err)
- defer os.RemoveAll(dir)
- f, err := os.CreateTemp(dir, "send")
- assert.NilError(t, err)
- defer f.Close()
- buf := make([]byte, 1024)
- fileSize := len(buf) * 1024 * 10
- for written := 0; written < fileSize; {
- n, err := f.Write(buf)
- assert.NilError(t, err)
- written += n
- }
- c := d.NewClientT(t)
- cID := container.Run(ctx, t, c)
- defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
- _, err = f.Seek(0, io.SeekStart)
- assert.NilError(t, err)
- srcInfo, err := archive.CopyInfoSourcePath(f.Name(), false)
- assert.NilError(t, err)
- srcArchive, err := archive.TarResource(srcInfo)
- assert.NilError(t, err)
- defer srcArchive.Close()
- dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, archive.CopyInfo{Path: "/test"})
- assert.NilError(t, err)
- err = c.CopyToContainer(ctx, cID, dstDir, preparedArchive, types.CopyToContainerOptions{})
- assert.NilError(t, err)
- rdr, _, err := c.CopyFromContainer(ctx, cID, "/test")
- assert.NilError(t, err)
- _, err = io.Copy(io.Discard, rdr)
- assert.NilError(t, err)
- }
- func imageSave(ctx context.Context, client client.APIClient, path, image string) error {
- responseReader, err := client.ImageSave(ctx, []string{image})
- if err != nil {
- return err
- }
- defer responseReader.Close()
- file, err := os.Create(path)
- if err != nil {
- return err
- }
- defer file.Close()
- _, err = io.Copy(file, responseReader)
- return err
- }
- func imageLoad(ctx context.Context, client client.APIClient, path string) error {
- file, err := os.Open(path)
- if err != nil {
- return err
- }
- defer file.Close()
- quiet := true
- response, err := client.ImageLoad(ctx, file, quiet)
- if err != nil {
- return err
- }
- defer response.Body.Close()
- return nil
- }
- func imageImport(ctx context.Context, client client.APIClient, path string) error {
- file, err := os.Open(path)
- if err != nil {
- return err
- }
- defer file.Close()
- options := types.ImageImportOptions{}
- ref := ""
- source := types.ImageImportSource{
- Source: file,
- SourceName: "-",
- }
- responseReader, err := client.ImageImport(ctx, source, ref, options)
- if err != nil {
- return err
- }
- defer responseReader.Close()
- return nil
- }
- func TestAuthZPluginHeader(t *testing.T) {
- ctx := setupTestV1(t)
- ctrl.reqRes.Allow = true
- ctrl.resRes.Allow = true
- d.StartWithBusybox(ctx, t, "--debug", "--authorization-plugin="+testAuthZPlugin)
- daemonURL, err := url.Parse(d.Sock())
- assert.NilError(t, err)
- conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
- assert.NilError(t, err)
- client := httputil.NewClientConn(conn, nil)
- req, err := http.NewRequest(http.MethodGet, "/version", nil)
- assert.NilError(t, err)
- req = req.WithContext(ctx)
- resp, err := client.Do(req)
- assert.NilError(t, err)
- assert.Equal(t, "application/json", resp.Header["Content-Type"][0])
- }
- // assertURIRecorded verifies that the given URI was sent and recorded
- // in the authz plugin
- func assertURIRecorded(t *testing.T, uris []string, uri string) {
- var found bool
- for _, u := range uris {
- if strings.Contains(u, uri) {
- found = true
- break
- }
- }
- if !found {
- t.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
- }
- }
|