mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	Docker authorization plug-in infrastructure enables extending the functionality of the Docker daemon with respect to user authorization. The infrastructure enables registering a set of external authorization plug-in. Each plug-in receives information about the user and the request and decides whether to allow or deny the request. Only in case all plug-ins allow accessing the resource the access is granted.
Each plug-in operates as a separate service, and registers with Docker through general (plug-ins API) [https://blog.docker.com/2015/06/extending-docker-with-plugins/]. No Docker daemon recompilation is required in order to add / remove an authentication plug-in. Each plug-in is notified twice for each operation: 1) before the operation is performed and, 2) before the response is returned to the client. The plug-ins can modify the response that is returned to the client. The authorization depends on the authorization effort that takes place in parallel [https://github.com/docker/docker/issues/13697]. This is the official issue of the authorization effort: https://github.com/docker/docker/issues/14674 (Here)[https://github.com/rhatdan/docker-rbac] you can find an open document that discusses a default RBAC plug-in for Docker. Signed-off-by: Liron Levin <liron@twistlock.com> Added container create flow test and extended the verification for ps
This commit is contained in:
		
							parent
							
								
									630f695fb1
								
							
						
					
					
						commit
						75c353f0ad
					
				
					 13 changed files with 1023 additions and 12 deletions
				
			
		| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	"github.com/docker/docker/api/server/httputils"
 | 
			
		||||
	"github.com/docker/docker/dockerversion"
 | 
			
		||||
	"github.com/docker/docker/errors"
 | 
			
		||||
	"github.com/docker/docker/pkg/authorization"
 | 
			
		||||
	"github.com/docker/docker/pkg/version"
 | 
			
		||||
	"golang.org/x/net/context"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +48,35 @@ func debugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// authorizationMiddleware perform authorization on the request.
 | 
			
		||||
func (s *Server) authorizationMiddleware(handler httputils.APIFunc) httputils.APIFunc {
 | 
			
		||||
	return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 | 
			
		||||
		// User and UserAuthNMethod are taken from AuthN plugins
 | 
			
		||||
		// Currently tracked in https://github.com/docker/docker/pull/13994
 | 
			
		||||
		user := ""
 | 
			
		||||
		userAuthNMethod := ""
 | 
			
		||||
		authCtx := authorization.NewCtx(s.authZPlugins, user, userAuthNMethod, r.Method, r.RequestURI)
 | 
			
		||||
 | 
			
		||||
		if err := authCtx.AuthZRequest(w, r); err != nil {
 | 
			
		||||
			logrus.Errorf("AuthZRequest for %s %s returned error: %s", r.Method, r.RequestURI, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rw := authorization.NewResponseModifier(w)
 | 
			
		||||
 | 
			
		||||
		if err := handler(ctx, rw, r, vars); err != nil {
 | 
			
		||||
			logrus.Errorf("Handler for %s %s returned error: %s", r.Method, r.RequestURI, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := authCtx.AuthZResponse(rw, r); err != nil {
 | 
			
		||||
			logrus.Errorf("AuthZResponse for %s %s returned error: %s", r.Method, r.RequestURI, err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// userAgentMiddleware checks the User-Agent header looking for a valid docker client spec.
 | 
			
		||||
func (s *Server) userAgentMiddleware(handler httputils.APIFunc) httputils.APIFunc {
 | 
			
		||||
	return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 | 
			
		||||
| 
						 | 
				
			
			@ -133,6 +163,11 @@ func (s *Server) handleWithGlobalMiddlewares(handler httputils.APIFunc) httputil
 | 
			
		|||
		middlewares = append(middlewares, debugRequestMiddleware)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(s.cfg.AuthZPluginNames) > 0 {
 | 
			
		||||
		s.authZPlugins = authorization.NewPlugins(s.cfg.AuthZPluginNames)
 | 
			
		||||
		middlewares = append(middlewares, s.authorizationMiddleware)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h := handler
 | 
			
		||||
	for _, m := range middlewares {
 | 
			
		||||
		h = m(h)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@ import (
 | 
			
		|||
	"github.com/docker/docker/api/server/router/system"
 | 
			
		||||
	"github.com/docker/docker/api/server/router/volume"
 | 
			
		||||
	"github.com/docker/docker/daemon"
 | 
			
		||||
	"github.com/docker/docker/pkg/authorization"
 | 
			
		||||
	"github.com/docker/docker/pkg/sockets"
 | 
			
		||||
	"github.com/docker/docker/utils"
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
| 
						 | 
				
			
			@ -28,13 +29,14 @@ const versionMatcher = "/v{version:[0-9.]+}"
 | 
			
		|||
 | 
			
		||||
// Config provides the configuration for the API server
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Logging     bool
 | 
			
		||||
	EnableCors  bool
 | 
			
		||||
	CorsHeaders string
 | 
			
		||||
	Version     string
 | 
			
		||||
	SocketGroup string
 | 
			
		||||
	TLSConfig   *tls.Config
 | 
			
		||||
	Addrs       []Addr
 | 
			
		||||
	Logging          bool
 | 
			
		||||
	EnableCors       bool
 | 
			
		||||
	CorsHeaders      string
 | 
			
		||||
	AuthZPluginNames []string
 | 
			
		||||
	Version          string
 | 
			
		||||
	SocketGroup      string
 | 
			
		||||
	TLSConfig        *tls.Config
 | 
			
		||||
	Addrs            []Addr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Server contains instance details for the server
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +44,7 @@ type Server struct {
 | 
			
		|||
	cfg     *Config
 | 
			
		||||
	servers []*HTTPServer
 | 
			
		||||
	routers []router.Router
 | 
			
		||||
	authZPlugins []authorization.Plugin
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Addr contains string representation of address and its protocol (tcp, unix...).
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ const (
 | 
			
		|||
// CommonConfig defines the configuration of a docker daemon which are
 | 
			
		||||
// common across platforms.
 | 
			
		||||
type CommonConfig struct {
 | 
			
		||||
	AuthZPlugins  []string // AuthZPlugins holds list of authorization plugins
 | 
			
		||||
	AutoRestart   bool
 | 
			
		||||
	Bridge        bridgeConfig // Bridge holds bridge network specific configuration.
 | 
			
		||||
	Context       map[string][]string
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +55,7 @@ type CommonConfig struct {
 | 
			
		|||
// from the command-line.
 | 
			
		||||
func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) {
 | 
			
		||||
	cmd.Var(opts.NewListOptsRef(&config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
 | 
			
		||||
	cmd.Var(opts.NewListOptsRef(&config.AuthZPlugins, nil), []string{"-authz-plugins"}, usageFn("List of authorization plugins by order of evaluation"))
 | 
			
		||||
	cmd.Var(opts.NewListOptsRef(&config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
 | 
			
		||||
	cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file"))
 | 
			
		||||
	cmd.StringVar(&config.Root, []string{"g", "-graph"}, defaultGraph, usageFn("Root of the Docker runtime"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -177,8 +177,9 @@ func (cli *DaemonCli) CmdDaemon(args ...string) error {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	serverConfig := &apiserver.Config{
 | 
			
		||||
		Logging: true,
 | 
			
		||||
		Version: dockerversion.Version,
 | 
			
		||||
		AuthZPluginNames: cli.Config.AuthZPlugins,
 | 
			
		||||
		Logging:          true,
 | 
			
		||||
		Version:          dockerversion.Version,
 | 
			
		||||
	}
 | 
			
		||||
	serverConfig = setPlatformServerConfig(serverConfig, cli.Config)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,9 +91,10 @@ Message | string | Authorization message (will be returned to the client in case
 | 
			
		|||
 | 
			
		||||
### Setting up docker daemon 
 | 
			
		||||
 | 
			
		||||
Authorization plugins are enabled with a dedicated command line argument. The argument contains a comma separated list of the plugin names, which should be the same as the plugin’s socket or spec file. 
 | 
			
		||||
Authorization plugins are enabled with a dedicated command line argument. The argument contains the plugin name, which should be the same as the plugin’s socket or spec file.
 | 
			
		||||
Multiple authz-plugin parameters are supported.
 | 
			
		||||
```
 | 
			
		||||
$ docker -d authz-plugins=plugin1,plugin2,...
 | 
			
		||||
$ docker daemon --authz-plugins=plugin1 --auth-plugins=plugin2,...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Calling authorized command (allow)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										228
									
								
								integration-cli/docker_cli_authz_unix_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								integration-cli/docker_cli_authz_unix_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,228 @@
 | 
			
		|||
// +build !windows
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/docker/docker/pkg/authorization"
 | 
			
		||||
	"github.com/docker/docker/pkg/integration/checker"
 | 
			
		||||
	"github.com/docker/docker/pkg/plugins"
 | 
			
		||||
	"github.com/go-check/check"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const testAuthZPlugin = "authzplugin"
 | 
			
		||||
const unauthorizedMessage = "User unauthorized authz plugin"
 | 
			
		||||
const containerListAPI = "/containers/json"
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	check.Suite(&DockerAuthzSuite{
 | 
			
		||||
		ds: &DockerSuite{},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DockerAuthzSuite struct {
 | 
			
		||||
	server *httptest.Server
 | 
			
		||||
	ds     *DockerSuite
 | 
			
		||||
	d      *Daemon
 | 
			
		||||
	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
 | 
			
		||||
	psRequestCnt  int                    // psRequestCnt counts the number of calls to list container request api
 | 
			
		||||
	psResponseCnt int                    // psResponseCnt counts the number of calls to list containers response API
 | 
			
		||||
	requestsURIs  []string               // requestsURIs stores all request URIs that are sent to the authorization controller
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
 | 
			
		||||
	s.d = NewDaemon(c)
 | 
			
		||||
	s.ctrl = &authorizationController{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
 | 
			
		||||
	s.d.Stop()
 | 
			
		||||
	s.ds.TearDownTest(c)
 | 
			
		||||
	s.ctrl = nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
 | 
			
		||||
	mux := http.NewServeMux()
 | 
			
		||||
	s.server = httptest.NewServer(mux)
 | 
			
		||||
	c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server"))
 | 
			
		||||
 | 
			
		||||
	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}})
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
		w.Write(b)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		b, err := json.Marshal(s.ctrl.reqRes)
 | 
			
		||||
		w.Write(b)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
		defer r.Body.Close()
 | 
			
		||||
		body, err := ioutil.ReadAll(r.Body)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
		authReq := authorization.Request{}
 | 
			
		||||
		err = json.Unmarshal(body, &authReq)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
 | 
			
		||||
		assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody)
 | 
			
		||||
		assertAuthHeaders(c, authReq.RequestHeaders)
 | 
			
		||||
 | 
			
		||||
		// Count only container list api
 | 
			
		||||
		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
 | 
			
		||||
			s.ctrl.psRequestCnt++
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		b, err := json.Marshal(s.ctrl.resRes)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
		w.Write(b)
 | 
			
		||||
 | 
			
		||||
		defer r.Body.Close()
 | 
			
		||||
		body, err := ioutil.ReadAll(r.Body)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
		authReq := authorization.Request{}
 | 
			
		||||
		err = json.Unmarshal(body, &authReq)
 | 
			
		||||
		c.Assert(err, check.IsNil)
 | 
			
		||||
 | 
			
		||||
		assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody)
 | 
			
		||||
		assertAuthHeaders(c, authReq.ResponseHeaders)
 | 
			
		||||
 | 
			
		||||
		// Count only container list api
 | 
			
		||||
		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
 | 
			
		||||
			s.ctrl.psResponseCnt++
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	err := os.MkdirAll("/etc/docker/plugins", 0755)
 | 
			
		||||
	c.Assert(err, checker.IsNil)
 | 
			
		||||
 | 
			
		||||
	fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
 | 
			
		||||
	err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644)
 | 
			
		||||
	c.Assert(err, checker.IsNil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// assertAuthHeaders validates authentication headers are removed
 | 
			
		||||
func assertAuthHeaders(c *check.C, headers map[string]string) error {
 | 
			
		||||
	for k := range headers {
 | 
			
		||||
		if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") {
 | 
			
		||||
			c.Errorf("Found authentication headers in request '%v'", headers)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// assertBody asserts that body is removed for non text/json requests
 | 
			
		||||
func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) {
 | 
			
		||||
 | 
			
		||||
	if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 {
 | 
			
		||||
		//return fmt.Errorf("Body included for authentication endpoint %s", string(body))
 | 
			
		||||
		c.Errorf("Body included for authentication endpoint %s", string(body))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for k, v := range headers {
 | 
			
		||||
		if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(body) > 0 {
 | 
			
		||||
		c.Errorf("Body included while it should not (Headers: '%v')", headers)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) TearDownSuite(c *check.C) {
 | 
			
		||||
	if s.server == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.server.Close()
 | 
			
		||||
 | 
			
		||||
	err := os.RemoveAll("/etc/docker/plugins")
 | 
			
		||||
	c.Assert(err, checker.IsNil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) {
 | 
			
		||||
 | 
			
		||||
	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
 | 
			
		||||
	c.Assert(err, check.IsNil)
 | 
			
		||||
	s.ctrl.reqRes.Allow = true
 | 
			
		||||
	s.ctrl.resRes.Allow = true
 | 
			
		||||
 | 
			
		||||
	// Ensure command successful
 | 
			
		||||
	out, err := s.d.Cmd("run", "-d", "--name", "container1", "busybox:latest", "top")
 | 
			
		||||
	c.Assert(err, check.IsNil)
 | 
			
		||||
 | 
			
		||||
	// Extract the id of the created container
 | 
			
		||||
	res := strings.Split(strings.TrimSpace(out), "\n")
 | 
			
		||||
	id := res[len(res)-1]
 | 
			
		||||
	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
 | 
			
		||||
	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id))
 | 
			
		||||
 | 
			
		||||
	out, err = s.d.Cmd("ps")
 | 
			
		||||
	c.Assert(err, check.IsNil)
 | 
			
		||||
	c.Assert(assertContainerList(out, []string{id}), check.Equals, true)
 | 
			
		||||
	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
 | 
			
		||||
	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
 | 
			
		||||
 | 
			
		||||
	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
 | 
			
		||||
	c.Assert(err, check.IsNil)
 | 
			
		||||
	s.ctrl.reqRes.Allow = false
 | 
			
		||||
	s.ctrl.reqRes.Msg = unauthorizedMessage
 | 
			
		||||
 | 
			
		||||
	// Ensure command is blocked
 | 
			
		||||
	res, err := s.d.Cmd("ps")
 | 
			
		||||
	c.Assert(err, check.NotNil)
 | 
			
		||||
	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
 | 
			
		||||
	c.Assert(s.ctrl.psResponseCnt, check.Equals, 0)
 | 
			
		||||
 | 
			
		||||
	// Ensure unauthorized message appears in response
 | 
			
		||||
	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: %s\n", unauthorizedMessage))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
 | 
			
		||||
 | 
			
		||||
	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
 | 
			
		||||
	c.Assert(err, check.IsNil)
 | 
			
		||||
	s.ctrl.reqRes.Allow = true
 | 
			
		||||
	s.ctrl.resRes.Allow = false
 | 
			
		||||
	s.ctrl.resRes.Msg = unauthorizedMessage
 | 
			
		||||
 | 
			
		||||
	// Ensure command is blocked
 | 
			
		||||
	res, err := s.d.Cmd("ps")
 | 
			
		||||
	c.Assert(err, check.NotNil)
 | 
			
		||||
	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
 | 
			
		||||
	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
 | 
			
		||||
 | 
			
		||||
	// Ensure unauthorized message appears in response
 | 
			
		||||
	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: %s\n", unauthorizedMessage))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
 | 
			
		||||
func assertURIRecorded(c *check.C, uris []string, uri string) {
 | 
			
		||||
 | 
			
		||||
	found := false
 | 
			
		||||
	for _, u := range uris {
 | 
			
		||||
		if strings.Contains(u, uri) {
 | 
			
		||||
			found = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !found {
 | 
			
		||||
		c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -133,7 +133,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
 | 
			
		|||
			// Check each line for lots of stuff
 | 
			
		||||
			lines := strings.Split(out, "\n")
 | 
			
		||||
			for _, line := range lines {
 | 
			
		||||
				c.Assert(len(line), checker.LessOrEqualThan, 90, check.Commentf("Help for %q is too long:\n%s", cmd, line))
 | 
			
		||||
				c.Assert(len(line), checker.LessOrEqualThan, 91, check.Commentf("Help for %q is too long:\n%s", cmd, line))
 | 
			
		||||
 | 
			
		||||
				if scanForHome && strings.Contains(line, `"`+home) {
 | 
			
		||||
					c.Fatalf("Help for %q should use ~ instead of %q on:\n%s",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										52
									
								
								pkg/authorization/api.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								pkg/authorization/api.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,52 @@
 | 
			
		|||
package authorization
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// AuthZApiRequest is the url for daemon request authorization
 | 
			
		||||
	AuthZApiRequest = "AuthZPlugin.AuthZReq"
 | 
			
		||||
 | 
			
		||||
	// AuthZApiResponse is the url for daemon response authorization
 | 
			
		||||
	AuthZApiResponse = "AuthZPlugin.AuthZRes"
 | 
			
		||||
 | 
			
		||||
	// AuthZApiImplements is the name of the interface all AuthZ plugins implement
 | 
			
		||||
	AuthZApiImplements = "authz"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Request holds data required for authZ plugins
 | 
			
		||||
type Request struct {
 | 
			
		||||
	// User holds the user extracted by AuthN mechanism
 | 
			
		||||
	User string `json:"User,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// UserAuthNMethod holds the mechanism used to extract user details (e.g., krb)
 | 
			
		||||
	UserAuthNMethod string `json:"UserAuthNMethod,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// RequestMethod holds the HTTP method (GET/POST/PUT)
 | 
			
		||||
	RequestMethod string `json:"RequestMethod,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// RequestUri holds the full HTTP uri (e.g., /v1.21/version)
 | 
			
		||||
	RequestURI string `json:"RequestUri,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// RequestBody stores the raw request body sent to the docker daemon
 | 
			
		||||
	RequestBody []byte `json:"RequestBody,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// RequestHeaders stores the raw request headers sent to the docker daemon
 | 
			
		||||
	RequestHeaders map[string]string `json:"RequestHeaders,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// ResponseStatusCode stores the status code returned from docker daemon
 | 
			
		||||
	ResponseStatusCode int `json:"ResponseStatusCode,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// ResponseBody stores the raw response body sent from docker daemon
 | 
			
		||||
	ResponseBody []byte `json:"ResponseBody,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// ResponseHeaders stores the response headers sent to the docker daemon
 | 
			
		||||
	ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Response represents authZ plugin response
 | 
			
		||||
type Response struct {
 | 
			
		||||
 | 
			
		||||
	// Allow indicating whether the user is allowed or not
 | 
			
		||||
	Allow bool `json:"Allow"`
 | 
			
		||||
 | 
			
		||||
	// Msg stores the authorization message
 | 
			
		||||
	Msg string `json:"Msg,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								pkg/authorization/api.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								pkg/authorization/api.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
# Docker Authorization Plug-in API
 | 
			
		||||
 | 
			
		||||
## Introduction
 | 
			
		||||
 | 
			
		||||
Docker authorization plug-in infrastructure enables extending the functionality of the Docker daemon with respect to user authorization. The infrastructure enables registering a set of external authorization plug-in. Each plug-in receives information about the user and the request and decides whether to allow or deny the request. Only in case all plug-ins allow accessing the resource the access is granted. 
 | 
			
		||||
 | 
			
		||||
Each plug-in operates as a separate service, and registers with Docker through general (plug-ins API) [https://blog.docker.com/2015/06/extending-docker-with-plugins/]. No Docker daemon recompilation is required in order to add / remove an authentication plug-in. Each plug-in is notified twice for each operation: 1) before the operation is performed and, 2) before the response is returned to the client. The plug-ins can modify the response that is returned to the client. 
 | 
			
		||||
 | 
			
		||||
The authorization depends on the authorization effort that takes place in parallel [https://github.com/docker/docker/issues/13697]. 
 | 
			
		||||
 | 
			
		||||
This is the official issue of the authorization effort: https://github.com/docker/docker/issues/14674
 | 
			
		||||
 | 
			
		||||
(Here)[https://github.com/rhatdan/docker-rbac] you can find an open document that discusses a default RBAC plug-in for Docker. 
 | 
			
		||||
 | 
			
		||||
## Docker daemon configuration 
 | 
			
		||||
 | 
			
		||||
In order to add a single authentication plug-in or a set of such, please use the following command line argument:
 | 
			
		||||
 | 
			
		||||
``` docker -d authz-plugin=authZPlugin1,authZPlugin2 ```
 | 
			
		||||
 | 
			
		||||
## API
 | 
			
		||||
 | 
			
		||||
The skeleton code for a typical plug-in can be found here [ADD LINK]. The plug-in must implement two AP methods:
 | 
			
		||||
 | 
			
		||||
1. */AuthzPlugin.AuthZReq* - this is the _authorize request_ method that is called before executing the Docker operation. 
 | 
			
		||||
1. */AuthzPlugin.AuthZRes* - this is the _authorize response_ method that is called before returning the response to the client. 
 | 
			
		||||
 | 
			
		||||
#### /AuthzPlugin.AuthZReq
 | 
			
		||||
 | 
			
		||||
**Request**:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
{    
 | 
			
		||||
    "User":              "The user identification"
 | 
			
		||||
    "UserAuthNMethod":   "The authentication method used"
 | 
			
		||||
    "RequestMethod":     "The HTTP method"
 | 
			
		||||
    "RequestUri":        "The HTTP request URI"
 | 
			
		||||
    "RequestBody":       "Byte array containing the raw HTTP request body"
 | 
			
		||||
    "RequestHeader":     "Byte array containing the raw HTTP request header as a map[string][]string "
 | 
			
		||||
    "RequestStatusCode": "Request status code"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Response**:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
{    
 | 
			
		||||
    "Allow" : "Determined whether the user is allowed or not"
 | 
			
		||||
    "Msg":    "The authorization message"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### /AuthzPlugin.AuthZRes
 | 
			
		||||
 | 
			
		||||
**Request**:
 | 
			
		||||
```
 | 
			
		||||
{
 | 
			
		||||
    "User":              "The user identification"
 | 
			
		||||
    "UserAuthNMethod":   "The authentication method used"
 | 
			
		||||
    "RequestMethod":     "The HTTP method"
 | 
			
		||||
    "RequestUri":        "The HTTP request URI"
 | 
			
		||||
    "RequestBody":       "Byte array containing the raw HTTP request body"
 | 
			
		||||
    "RequestHeader":     "Byte array containing the raw HTTP request header as a map[string][]string"
 | 
			
		||||
    "RequestStatusCode": "Request status code"
 | 
			
		||||
    "ResponseBody":      "Byte array containing the raw HTTP response body"
 | 
			
		||||
    "ResponseHeader":    "Byte array containing the raw HTTP response header as a map[string][]string"
 | 
			
		||||
    "ResponseStatusCode":"Response status code"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Response**:
 | 
			
		||||
```
 | 
			
		||||
{
 | 
			
		||||
   "Allow" :               "Determined whether the user is allowed or not"
 | 
			
		||||
   "Msg":                  "The authorization message"
 | 
			
		||||
   "ModifiedBody":         "Byte array containing a modified body of the raw HTTP body (or nil if no changes required)"
 | 
			
		||||
   "ModifiedHeader":       "Byte array containing a modified header of the HTTP response (or nil if no changes required)"
 | 
			
		||||
   "ModifiedStatusCode":   "int containing the modified version of the status code (or 0 if not change is required)"
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The modified response enables the authorization plug-in to manipulate the content of the HTTP response.
 | 
			
		||||
In case of more than one plug-in, each subsequent plug-in will received a response (optionally) modified by a previous plug-in. 
 | 
			
		||||
							
								
								
									
										159
									
								
								pkg/authorization/authz.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								pkg/authorization/authz.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,159 @@
 | 
			
		|||
package authorization
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// NewCtx creates new authZ context, it is used to store authorization information related to a specific docker
 | 
			
		||||
// REST http session
 | 
			
		||||
// A context provides two method:
 | 
			
		||||
// Authenticate Request:
 | 
			
		||||
// Call authZ plugins with current REST request and AuthN response
 | 
			
		||||
// Request contains full HTTP packet sent to the docker daemon
 | 
			
		||||
// https://docs.docker.com/reference/api/docker_remote_api/
 | 
			
		||||
//
 | 
			
		||||
// Authenticate Response:
 | 
			
		||||
// Call authZ plugins with full info about current REST request, REST response and AuthN response
 | 
			
		||||
// The response from this method may contains content that overrides the daemon response
 | 
			
		||||
// This allows authZ plugins to filter privileged content
 | 
			
		||||
//
 | 
			
		||||
// If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results
 | 
			
		||||
// For response manipulation, the response from each plugin is piped between plugins. Plugin execution order
 | 
			
		||||
// is determined according to daemon parameters
 | 
			
		||||
func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx {
 | 
			
		||||
	return &Ctx{plugins: authZPlugins, user: user, userAuthNMethod: userAuthNMethod, requestMethod: requestMethod, requestURI: requestURI}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Ctx stores a a single request-response interaction context
 | 
			
		||||
type Ctx struct {
 | 
			
		||||
	user            string
 | 
			
		||||
	userAuthNMethod string
 | 
			
		||||
	requestMethod   string
 | 
			
		||||
	requestURI      string
 | 
			
		||||
	plugins         []Plugin
 | 
			
		||||
	// authReq stores the cached request object for the current transaction
 | 
			
		||||
	authReq *Request
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthZRequest authorized the request to the docker daemon using authZ plugins
 | 
			
		||||
func (a *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) (err error) {
 | 
			
		||||
 | 
			
		||||
	var body []byte
 | 
			
		||||
	if sendBody(a.requestURI, r.Header) {
 | 
			
		||||
		var drainedBody io.ReadCloser
 | 
			
		||||
		drainedBody, r.Body, err = drainBody(r.Body)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		body, err = ioutil.ReadAll(drainedBody)
 | 
			
		||||
		defer drainedBody.Close()
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var h bytes.Buffer
 | 
			
		||||
	err = r.Header.Write(&h)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	a.authReq = &Request{
 | 
			
		||||
		User:            a.user,
 | 
			
		||||
		UserAuthNMethod: a.userAuthNMethod,
 | 
			
		||||
		RequestMethod:   a.requestMethod,
 | 
			
		||||
		RequestURI:      a.requestURI,
 | 
			
		||||
		RequestBody:     body,
 | 
			
		||||
		RequestHeaders:  headers(r.Header)}
 | 
			
		||||
 | 
			
		||||
	for _, plugin := range a.plugins {
 | 
			
		||||
 | 
			
		||||
		authRes, err := plugin.AuthZRequest(a.authReq)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !authRes.Allow {
 | 
			
		||||
			return fmt.Errorf(authRes.Msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
 | 
			
		||||
func (a *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
 | 
			
		||||
 | 
			
		||||
	a.authReq.ResponseStatusCode = rm.StatusCode()
 | 
			
		||||
	a.authReq.ResponseHeaders = headers(rm.Header())
 | 
			
		||||
 | 
			
		||||
	if sendBody(a.requestURI, rm.Header()) {
 | 
			
		||||
		a.authReq.ResponseBody = rm.RawBody()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, plugin := range a.plugins {
 | 
			
		||||
 | 
			
		||||
		authRes, err := plugin.AuthZResponse(a.authReq)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !authRes.Allow {
 | 
			
		||||
			return fmt.Errorf(authRes.Msg)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rm.Flush()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// drainBody dump the body, it reads the body data into memory and
 | 
			
		||||
// see go sources /go/src/net/http/httputil/dump.go
 | 
			
		||||
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
 | 
			
		||||
	var buf bytes.Buffer
 | 
			
		||||
	if _, err = buf.ReadFrom(b); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err = b.Close(); err != nil {
 | 
			
		||||
		return nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendBody returns true when request/response body should be sent to AuthZPlugin
 | 
			
		||||
func sendBody(url string, header http.Header) bool {
 | 
			
		||||
 | 
			
		||||
	// Skip body for auth endpoint
 | 
			
		||||
	if strings.HasSuffix(url, "/auth") {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// body is sent only for text or json messages
 | 
			
		||||
	v := header.Get("Content-Type")
 | 
			
		||||
	return strings.HasPrefix(v, "text/") || v == "application/json"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// headers returns flatten version of the http headers excluding authorization
 | 
			
		||||
func headers(header http.Header) map[string]string {
 | 
			
		||||
	v := make(map[string]string, 0)
 | 
			
		||||
	for k, values := range header {
 | 
			
		||||
		// Skip authorization headers
 | 
			
		||||
		if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		for _, val := range values {
 | 
			
		||||
			v[k] = val
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return v
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										220
									
								
								pkg/authorization/authz_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								pkg/authorization/authz_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,220 @@
 | 
			
		|||
package authorization
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/docker/docker/pkg/plugins"
 | 
			
		||||
	"github.com/docker/docker/pkg/tlsconfig"
 | 
			
		||||
	"github.com/gorilla/mux"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const pluginAddress = "authzplugin.sock"
 | 
			
		||||
 | 
			
		||||
func TestAuthZRequestPlugin(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	server := authZPluginTestServer{t: t}
 | 
			
		||||
	go server.start()
 | 
			
		||||
	defer server.stop()
 | 
			
		||||
 | 
			
		||||
	authZPlugin := createTestPlugin(t)
 | 
			
		||||
 | 
			
		||||
	request := Request{
 | 
			
		||||
		User:           "user",
 | 
			
		||||
		RequestBody:    []byte("sample body"),
 | 
			
		||||
		RequestURI:     "www.authz.com",
 | 
			
		||||
		RequestMethod:  "GET",
 | 
			
		||||
		RequestHeaders: map[string]string{"header": "value"},
 | 
			
		||||
	}
 | 
			
		||||
	server.replayResponse = Response{
 | 
			
		||||
		Allow: true,
 | 
			
		||||
		Msg:   "Sample message",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	actualResponse, err := authZPlugin.AuthZRequest(&request)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Failed to authorize request %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
 | 
			
		||||
		t.Fatalf("Response must be equal")
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(request, server.recordedRequest) {
 | 
			
		||||
		t.Fatalf("Requests must be equal")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestAuthZResponsePlugin(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	server := authZPluginTestServer{t: t}
 | 
			
		||||
	go server.start()
 | 
			
		||||
	defer server.stop()
 | 
			
		||||
 | 
			
		||||
	authZPlugin := createTestPlugin(t)
 | 
			
		||||
 | 
			
		||||
	request := Request{
 | 
			
		||||
		User:        "user",
 | 
			
		||||
		RequestBody: []byte("sample body"),
 | 
			
		||||
	}
 | 
			
		||||
	server.replayResponse = Response{
 | 
			
		||||
		Allow: true,
 | 
			
		||||
		Msg:   "Sample message",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	actualResponse, err := authZPlugin.AuthZResponse(&request)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Failed to authorize request %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
 | 
			
		||||
		t.Fatalf("Response must be equal")
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(request, server.recordedRequest) {
 | 
			
		||||
		t.Fatalf("Requests must be equal")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResponseModifier(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	r := httptest.NewRecorder()
 | 
			
		||||
	m := NewResponseModifier(r)
 | 
			
		||||
	m.Header().Set("h1", "v1")
 | 
			
		||||
	m.Write([]byte("body"))
 | 
			
		||||
	m.WriteHeader(500)
 | 
			
		||||
 | 
			
		||||
	m.Flush()
 | 
			
		||||
	if r.Header().Get("h1") != "v1" {
 | 
			
		||||
		t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(r.Body.Bytes(), []byte("body")) {
 | 
			
		||||
		t.Fatalf("Body value must exists %s", r.Body.Bytes())
 | 
			
		||||
	}
 | 
			
		||||
	if r.Code != 500 {
 | 
			
		||||
		t.Fatalf("Status code must be correct %d", r.Code)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResponseModifierOverride(t *testing.T) {
 | 
			
		||||
 | 
			
		||||
	r := httptest.NewRecorder()
 | 
			
		||||
	m := NewResponseModifier(r)
 | 
			
		||||
	m.Header().Set("h1", "v1")
 | 
			
		||||
	m.Write([]byte("body"))
 | 
			
		||||
	m.WriteHeader(500)
 | 
			
		||||
 | 
			
		||||
	overrideHeader := make(http.Header)
 | 
			
		||||
	overrideHeader.Add("h1", "v2")
 | 
			
		||||
	overrideHeaderBytes, err := json.Marshal(overrideHeader)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("override header failed %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.OverrideHeader(overrideHeaderBytes)
 | 
			
		||||
	m.OverrideBody([]byte("override body"))
 | 
			
		||||
	m.OverrideStatusCode(404)
 | 
			
		||||
	m.Flush()
 | 
			
		||||
	if r.Header().Get("h1") != "v2" {
 | 
			
		||||
		t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
 | 
			
		||||
	}
 | 
			
		||||
	if !reflect.DeepEqual(r.Body.Bytes(), []byte("override body")) {
 | 
			
		||||
		t.Fatalf("Body value must exists %s", r.Body.Bytes())
 | 
			
		||||
	}
 | 
			
		||||
	if r.Code != 404 {
 | 
			
		||||
		t.Fatalf("Status code must be correct %d", r.Code)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createTestPlugin creates a new sample authorization plugin
 | 
			
		||||
func createTestPlugin(t *testing.T) *authorizationPlugin {
 | 
			
		||||
	plugin := &plugins.Plugin{Name: "authz"}
 | 
			
		||||
	var err error
 | 
			
		||||
	pwd, err := os.Getwd()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Println(err)
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Failed to create client %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &authorizationPlugin{name: "plugin", plugin: plugin}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthZPluginTestServer is a simple server that implements the authZ plugin interface
 | 
			
		||||
type authZPluginTestServer struct {
 | 
			
		||||
	listener net.Listener
 | 
			
		||||
	t        *testing.T
 | 
			
		||||
	// request stores the request sent from the daemon to the plugin
 | 
			
		||||
	recordedRequest Request
 | 
			
		||||
	// response stores the response sent from the plugin to the daemon
 | 
			
		||||
	replayResponse Response
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// start starts the test server that implements the plugin
 | 
			
		||||
func (t *authZPluginTestServer) start() {
 | 
			
		||||
	r := mux.NewRouter()
 | 
			
		||||
	os.Remove(pluginAddress)
 | 
			
		||||
	l, err := net.ListenUnix("unix", &net.UnixAddr{Name: pluginAddress, Net: "unix"})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.t.Fatalf("Failed to listen %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	t.listener = l
 | 
			
		||||
 | 
			
		||||
	r.HandleFunc("/Plugin.Activate", t.activate)
 | 
			
		||||
	r.HandleFunc("/"+AuthZApiRequest, t.auth)
 | 
			
		||||
	r.HandleFunc("/"+AuthZApiResponse, t.auth)
 | 
			
		||||
	t.listener, err = net.Listen("tcp", pluginAddress)
 | 
			
		||||
	server := http.Server{Handler: r, Addr: pluginAddress}
 | 
			
		||||
	server.Serve(l)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// stop stops the test server that implements the plugin
 | 
			
		||||
func (t *authZPluginTestServer) stop() {
 | 
			
		||||
 | 
			
		||||
	os.Remove(pluginAddress)
 | 
			
		||||
 | 
			
		||||
	if t.listener != nil {
 | 
			
		||||
		t.listener.Close()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// auth is a used to record/replay the authentication api messages
 | 
			
		||||
func (t *authZPluginTestServer) auth(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	t.recordedRequest = Request{}
 | 
			
		||||
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
	body, err := ioutil.ReadAll(r.Body)
 | 
			
		||||
	json.Unmarshal(body, &t.recordedRequest)
 | 
			
		||||
	b, err := json.Marshal(t.replayResponse)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	w.Write(b)
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *authZPluginTestServer) activate(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	b, err := json.Marshal(plugins.Manifest{Implements: []string{AuthZApiImplements}})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	w.Write(b)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										87
									
								
								pkg/authorization/plugin.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								pkg/authorization/plugin.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
package authorization
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"github.com/Sirupsen/logrus"
 | 
			
		||||
	"github.com/docker/docker/pkg/plugins"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Plugin allows third party plugins to authorize requests and responses
 | 
			
		||||
// in the context of docker API
 | 
			
		||||
type Plugin interface {
 | 
			
		||||
 | 
			
		||||
	// AuthZRequest authorize the request from the client to the daemon
 | 
			
		||||
	AuthZRequest(authReq *Request) (authRes *Response, err error)
 | 
			
		||||
 | 
			
		||||
	// AuthZResponse authorize the response from the daemon to the client
 | 
			
		||||
	AuthZResponse(authReq *Request) (authRes *Response, err error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewPlugins constructs and initialize the authorization plugins based on plugin names
 | 
			
		||||
func NewPlugins(names []string) []Plugin {
 | 
			
		||||
	plugins := make([]Plugin, len(names))
 | 
			
		||||
	for i, name := range names {
 | 
			
		||||
		plugins[i] = newAuthorizationPlugin(name)
 | 
			
		||||
	}
 | 
			
		||||
	return plugins
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// authorizationPlugin is an internal adapter to docker plugin system
 | 
			
		||||
type authorizationPlugin struct {
 | 
			
		||||
	plugin *plugins.Plugin
 | 
			
		||||
	name   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newAuthorizationPlugin(name string) Plugin {
 | 
			
		||||
	return &authorizationPlugin{name: name}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authorizationPlugin) AuthZRequest(authReq *Request) (authRes *Response, err error) {
 | 
			
		||||
 | 
			
		||||
	logrus.Debugf("AuthZ requset using plugins %s", a.name)
 | 
			
		||||
 | 
			
		||||
	err = a.initPlugin()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authRes = &Response{}
 | 
			
		||||
	err = a.plugin.Client.Call(AuthZApiRequest, authReq, authRes)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return authRes, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *authorizationPlugin) AuthZResponse(authReq *Request) (authRes *Response, err error) {
 | 
			
		||||
 | 
			
		||||
	logrus.Debugf("AuthZ response using plugins %s", a.name)
 | 
			
		||||
 | 
			
		||||
	err = a.initPlugin()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	authRes = &Response{}
 | 
			
		||||
	err = a.plugin.Client.Call(AuthZApiResponse, authReq, authRes)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return authRes, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// initPlugin initialize the authorization plugin if needed
 | 
			
		||||
func (a *authorizationPlugin) initPlugin() (err error) {
 | 
			
		||||
 | 
			
		||||
	// Lazy loading of plugins
 | 
			
		||||
	if a.plugin == nil {
 | 
			
		||||
		a.plugin, err = plugins.Get(a.name, AuthZApiImplements)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								pkg/authorization/response.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								pkg/authorization/response.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,140 @@
 | 
			
		|||
package authorization
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bufio"
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ResponseModifier allows authorization plugins to read and modify the content of the http.response
 | 
			
		||||
type ResponseModifier interface {
 | 
			
		||||
	http.ResponseWriter
 | 
			
		||||
 | 
			
		||||
	// RawBody returns the current http content
 | 
			
		||||
	RawBody() []byte
 | 
			
		||||
 | 
			
		||||
	// RawHeaders returns the current content of the http headers
 | 
			
		||||
	RawHeaders() ([]byte, error)
 | 
			
		||||
 | 
			
		||||
	// StatusCode returns the current status code
 | 
			
		||||
	StatusCode() int
 | 
			
		||||
 | 
			
		||||
	// OverrideBody replace the body of the HTTP reply
 | 
			
		||||
	OverrideBody(b []byte)
 | 
			
		||||
 | 
			
		||||
	// OverrideHeader replace the headers of the HTTP reply
 | 
			
		||||
	OverrideHeader(b []byte) error
 | 
			
		||||
 | 
			
		||||
	// OverrideStatusCode replaces the status code of the HTTP reply
 | 
			
		||||
	OverrideStatusCode(statusCode int)
 | 
			
		||||
 | 
			
		||||
	// Flush flushes all data to the HTTP response
 | 
			
		||||
	Flush() error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewResponseModifier creates a wrapper to an http.ResponseWriter to allow inspecting and modifying the content
 | 
			
		||||
func NewResponseModifier(rw http.ResponseWriter) ResponseModifier {
 | 
			
		||||
	return &responseModifier{rw: rw, header: make(http.Header)}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// responseModifier is used as an adapter to http.ResponseWriter in order to manipulate and explore
 | 
			
		||||
// the http request/response from docker daemon
 | 
			
		||||
type responseModifier struct {
 | 
			
		||||
	// The original response writer
 | 
			
		||||
	rw     http.ResponseWriter
 | 
			
		||||
	status int
 | 
			
		||||
	// body holds the response body
 | 
			
		||||
	body []byte
 | 
			
		||||
	// header holds the response header
 | 
			
		||||
	header http.Header
 | 
			
		||||
	// statusCode holds the response status code
 | 
			
		||||
	statusCode int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriterHeader stores the http status code
 | 
			
		||||
func (rm *responseModifier) WriteHeader(s int) {
 | 
			
		||||
	rm.statusCode = s
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Header returns the internal http header
 | 
			
		||||
func (rm *responseModifier) Header() http.Header {
 | 
			
		||||
	return rm.header
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Header returns the internal http header
 | 
			
		||||
func (rm *responseModifier) StatusCode() int {
 | 
			
		||||
	return rm.statusCode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Override replace the body of the HTTP reply
 | 
			
		||||
func (rm *responseModifier) OverrideBody(b []byte) {
 | 
			
		||||
	rm.body = b
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rm *responseModifier) OverrideStatusCode(statusCode int) {
 | 
			
		||||
	rm.statusCode = statusCode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Override replace the headers of the HTTP reply
 | 
			
		||||
func (rm *responseModifier) OverrideHeader(b []byte) error {
 | 
			
		||||
	header := http.Header{}
 | 
			
		||||
	err := json.Unmarshal(b, &header)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	rm.header = header
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Write stores the byte array inside content
 | 
			
		||||
func (rm *responseModifier) Write(b []byte) (int, error) {
 | 
			
		||||
	rm.body = append(rm.body, b...)
 | 
			
		||||
	return len(b), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Body returns the response body
 | 
			
		||||
func (rm *responseModifier) RawBody() []byte {
 | 
			
		||||
	return rm.body
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (rm *responseModifier) RawHeaders() ([]byte, error) {
 | 
			
		||||
	var b bytes.Buffer
 | 
			
		||||
	err := rm.header.Write(&b)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return b.Bytes(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Hijack returns the internal connection of the wrapped http.ResponseWriter
 | 
			
		||||
func (rm *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
 | 
			
		||||
	hijacker, ok := rm.rw.(http.Hijacker)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, nil, fmt.Errorf("Internal reponse writer doesn't support the Hijacker interface")
 | 
			
		||||
	}
 | 
			
		||||
	return hijacker.Hijack()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Flush flushes all data to the HTTP response
 | 
			
		||||
func (rm *responseModifier) Flush() error {
 | 
			
		||||
 | 
			
		||||
	// Copy the status code
 | 
			
		||||
	if rm.statusCode > 0 {
 | 
			
		||||
		rm.rw.WriteHeader(rm.statusCode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Copy the header
 | 
			
		||||
	for k, vv := range rm.header {
 | 
			
		||||
		for _, v := range vv {
 | 
			
		||||
			rm.rw.Header().Add(k, v)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Write body
 | 
			
		||||
	_, err := rm.rw.Write(rm.body)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue