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  }