Browse code

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

Liron Levin authored on 2015/11/12 20:06:47
Showing 13 changed files
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"github.com/docker/docker/api/server/httputils"
14 14
 	"github.com/docker/docker/dockerversion"
15 15
 	"github.com/docker/docker/errors"
16
+	"github.com/docker/docker/pkg/authorization"
16 17
 	"github.com/docker/docker/pkg/version"
17 18
 	"golang.org/x/net/context"
18 19
 )
... ...
@@ -47,6 +48,35 @@ func debugRequestMiddleware(handler httputils.APIFunc) httputils.APIFunc {
47 47
 	}
48 48
 }
49 49
 
50
+// authorizationMiddleware perform authorization on the request.
51
+func (s *Server) authorizationMiddleware(handler httputils.APIFunc) httputils.APIFunc {
52
+	return func(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
53
+		// User and UserAuthNMethod are taken from AuthN plugins
54
+		// Currently tracked in https://github.com/docker/docker/pull/13994
55
+		user := ""
56
+		userAuthNMethod := ""
57
+		authCtx := authorization.NewCtx(s.authZPlugins, user, userAuthNMethod, r.Method, r.RequestURI)
58
+
59
+		if err := authCtx.AuthZRequest(w, r); err != nil {
60
+			logrus.Errorf("AuthZRequest for %s %s returned error: %s", r.Method, r.RequestURI, err)
61
+			return err
62
+		}
63
+
64
+		rw := authorization.NewResponseModifier(w)
65
+
66
+		if err := handler(ctx, rw, r, vars); err != nil {
67
+			logrus.Errorf("Handler for %s %s returned error: %s", r.Method, r.RequestURI, err)
68
+			return err
69
+		}
70
+
71
+		if err := authCtx.AuthZResponse(rw, r); err != nil {
72
+			logrus.Errorf("AuthZResponse for %s %s returned error: %s", r.Method, r.RequestURI, err)
73
+			return err
74
+		}
75
+		return nil
76
+	}
77
+}
78
+
50 79
 // userAgentMiddleware checks the User-Agent header looking for a valid docker client spec.
51 80
 func (s *Server) userAgentMiddleware(handler httputils.APIFunc) httputils.APIFunc {
52 81
 	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
133 133
 		middlewares = append(middlewares, debugRequestMiddleware)
134 134
 	}
135 135
 
136
+	if len(s.cfg.AuthZPluginNames) > 0 {
137
+		s.authZPlugins = authorization.NewPlugins(s.cfg.AuthZPluginNames)
138
+		middlewares = append(middlewares, s.authorizationMiddleware)
139
+	}
140
+
136 141
 	h := handler
137 142
 	for _, m := range middlewares {
138 143
 		h = m(h)
... ...
@@ -16,6 +16,7 @@ import (
16 16
 	"github.com/docker/docker/api/server/router/system"
17 17
 	"github.com/docker/docker/api/server/router/volume"
18 18
 	"github.com/docker/docker/daemon"
19
+	"github.com/docker/docker/pkg/authorization"
19 20
 	"github.com/docker/docker/pkg/sockets"
20 21
 	"github.com/docker/docker/utils"
21 22
 	"github.com/gorilla/mux"
... ...
@@ -28,13 +29,14 @@ const versionMatcher = "/v{version:[0-9.]+}"
28 28
 
29 29
 // Config provides the configuration for the API server
30 30
 type Config struct {
31
-	Logging     bool
32
-	EnableCors  bool
33
-	CorsHeaders string
34
-	Version     string
35
-	SocketGroup string
36
-	TLSConfig   *tls.Config
37
-	Addrs       []Addr
31
+	Logging          bool
32
+	EnableCors       bool
33
+	CorsHeaders      string
34
+	AuthZPluginNames []string
35
+	Version          string
36
+	SocketGroup      string
37
+	TLSConfig        *tls.Config
38
+	Addrs            []Addr
38 39
 }
39 40
 
40 41
 // Server contains instance details for the server
... ...
@@ -42,6 +44,7 @@ type Server struct {
42 42
 	cfg     *Config
43 43
 	servers []*HTTPServer
44 44
 	routers []router.Router
45
+	authZPlugins []authorization.Plugin
45 46
 }
46 47
 
47 48
 // Addr contains string representation of address and its protocol (tcp, unix...).
... ...
@@ -14,6 +14,7 @@ const (
14 14
 // CommonConfig defines the configuration of a docker daemon which are
15 15
 // common across platforms.
16 16
 type CommonConfig struct {
17
+	AuthZPlugins  []string // AuthZPlugins holds list of authorization plugins
17 18
 	AutoRestart   bool
18 19
 	Bridge        bridgeConfig // Bridge holds bridge network specific configuration.
19 20
 	Context       map[string][]string
... ...
@@ -54,6 +55,7 @@ type CommonConfig struct {
54 54
 // from the command-line.
55 55
 func (config *Config) InstallCommonFlags(cmd *flag.FlagSet, usageFn func(string) string) {
56 56
 	cmd.Var(opts.NewListOptsRef(&config.GraphOptions, nil), []string{"-storage-opt"}, usageFn("Set storage driver options"))
57
+	cmd.Var(opts.NewListOptsRef(&config.AuthZPlugins, nil), []string{"-authz-plugins"}, usageFn("List of authorization plugins by order of evaluation"))
57 58
 	cmd.Var(opts.NewListOptsRef(&config.ExecOptions, nil), []string{"-exec-opt"}, usageFn("Set exec driver options"))
58 59
 	cmd.StringVar(&config.Pidfile, []string{"p", "-pidfile"}, defaultPidFile, usageFn("Path to use for daemon PID file"))
59 60
 	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 {
177 177
 	}
178 178
 
179 179
 	serverConfig := &apiserver.Config{
180
-		Logging: true,
181
-		Version: dockerversion.Version,
180
+		AuthZPluginNames: cli.Config.AuthZPlugins,
181
+		Logging:          true,
182
+		Version:          dockerversion.Version,
182 183
 	}
183 184
 	serverConfig = setPlatformServerConfig(serverConfig, cli.Config)
184 185
 
... ...
@@ -91,9 +91,10 @@ Message | string | Authorization message (will be returned to the client in case
91 91
 
92 92
 ### Setting up docker daemon 
93 93
 
94
-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. 
94
+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.
95
+Multiple authz-plugin parameters are supported.
95 96
 ```
96
-$ docker -d authz-plugins=plugin1,plugin2,...
97
+$ docker daemon --authz-plugins=plugin1 --auth-plugins=plugin2,...
97 98
 ```
98 99
 
99 100
 ### Calling authorized command (allow)
100 101
new file mode 100644
... ...
@@ -0,0 +1,228 @@
0
+// +build !windows
1
+
2
+package main
3
+
4
+import (
5
+	"encoding/json"
6
+	"fmt"
7
+	"github.com/docker/docker/pkg/authorization"
8
+	"github.com/docker/docker/pkg/integration/checker"
9
+	"github.com/docker/docker/pkg/plugins"
10
+	"github.com/go-check/check"
11
+	"io/ioutil"
12
+	"net/http"
13
+	"net/http/httptest"
14
+	"os"
15
+	"strings"
16
+)
17
+
18
+const testAuthZPlugin = "authzplugin"
19
+const unauthorizedMessage = "User unauthorized authz plugin"
20
+const containerListAPI = "/containers/json"
21
+
22
+func init() {
23
+	check.Suite(&DockerAuthzSuite{
24
+		ds: &DockerSuite{},
25
+	})
26
+}
27
+
28
+type DockerAuthzSuite struct {
29
+	server *httptest.Server
30
+	ds     *DockerSuite
31
+	d      *Daemon
32
+	ctrl   *authorizationController
33
+}
34
+
35
+type authorizationController struct {
36
+	reqRes        authorization.Response // reqRes holds the plugin response to the initial client request
37
+	resRes        authorization.Response // resRes holds the plugin response to the daemon response
38
+	psRequestCnt  int                    // psRequestCnt counts the number of calls to list container request api
39
+	psResponseCnt int                    // psResponseCnt counts the number of calls to list containers response API
40
+	requestsURIs  []string               // requestsURIs stores all request URIs that are sent to the authorization controller
41
+
42
+}
43
+
44
+func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
45
+	s.d = NewDaemon(c)
46
+	s.ctrl = &authorizationController{}
47
+}
48
+
49
+func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
50
+	s.d.Stop()
51
+	s.ds.TearDownTest(c)
52
+	s.ctrl = nil
53
+}
54
+
55
+func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
56
+	mux := http.NewServeMux()
57
+	s.server = httptest.NewServer(mux)
58
+	c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server"))
59
+
60
+	mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
61
+		b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}})
62
+		c.Assert(err, check.IsNil)
63
+		w.Write(b)
64
+	})
65
+
66
+	mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) {
67
+		b, err := json.Marshal(s.ctrl.reqRes)
68
+		w.Write(b)
69
+		c.Assert(err, check.IsNil)
70
+		defer r.Body.Close()
71
+		body, err := ioutil.ReadAll(r.Body)
72
+		c.Assert(err, check.IsNil)
73
+		authReq := authorization.Request{}
74
+		err = json.Unmarshal(body, &authReq)
75
+		c.Assert(err, check.IsNil)
76
+
77
+		assertBody(c, authReq.RequestURI, authReq.RequestHeaders, authReq.RequestBody)
78
+		assertAuthHeaders(c, authReq.RequestHeaders)
79
+
80
+		// Count only container list api
81
+		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
82
+			s.ctrl.psRequestCnt++
83
+		}
84
+
85
+		s.ctrl.requestsURIs = append(s.ctrl.requestsURIs, authReq.RequestURI)
86
+	})
87
+
88
+	mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) {
89
+		b, err := json.Marshal(s.ctrl.resRes)
90
+		c.Assert(err, check.IsNil)
91
+		w.Write(b)
92
+
93
+		defer r.Body.Close()
94
+		body, err := ioutil.ReadAll(r.Body)
95
+		c.Assert(err, check.IsNil)
96
+		authReq := authorization.Request{}
97
+		err = json.Unmarshal(body, &authReq)
98
+		c.Assert(err, check.IsNil)
99
+
100
+		assertBody(c, authReq.RequestURI, authReq.ResponseHeaders, authReq.ResponseBody)
101
+		assertAuthHeaders(c, authReq.ResponseHeaders)
102
+
103
+		// Count only container list api
104
+		if strings.HasSuffix(authReq.RequestURI, containerListAPI) {
105
+			s.ctrl.psResponseCnt++
106
+		}
107
+	})
108
+
109
+	err := os.MkdirAll("/etc/docker/plugins", 0755)
110
+	c.Assert(err, checker.IsNil)
111
+
112
+	fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", testAuthZPlugin)
113
+	err = ioutil.WriteFile(fileName, []byte(s.server.URL), 0644)
114
+	c.Assert(err, checker.IsNil)
115
+}
116
+
117
+// assertAuthHeaders validates authentication headers are removed
118
+func assertAuthHeaders(c *check.C, headers map[string]string) error {
119
+	for k := range headers {
120
+		if strings.Contains(strings.ToLower(k), "auth") || strings.Contains(strings.ToLower(k), "x-registry") {
121
+			c.Errorf("Found authentication headers in request '%v'", headers)
122
+		}
123
+	}
124
+	return nil
125
+}
126
+
127
+// assertBody asserts that body is removed for non text/json requests
128
+func assertBody(c *check.C, requestURI string, headers map[string]string, body []byte) {
129
+
130
+	if strings.Contains(strings.ToLower(requestURI), "auth") && len(body) > 0 {
131
+		//return fmt.Errorf("Body included for authentication endpoint %s", string(body))
132
+		c.Errorf("Body included for authentication endpoint %s", string(body))
133
+	}
134
+
135
+	for k, v := range headers {
136
+		if strings.EqualFold(k, "Content-Type") && strings.HasPrefix(v, "text/") || v == "application/json" {
137
+			return
138
+		}
139
+	}
140
+	if len(body) > 0 {
141
+		c.Errorf("Body included while it should not (Headers: '%v')", headers)
142
+	}
143
+}
144
+
145
+func (s *DockerAuthzSuite) TearDownSuite(c *check.C) {
146
+	if s.server == nil {
147
+		return
148
+	}
149
+
150
+	s.server.Close()
151
+
152
+	err := os.RemoveAll("/etc/docker/plugins")
153
+	c.Assert(err, checker.IsNil)
154
+}
155
+
156
+func (s *DockerAuthzSuite) TestAuthZPluginAllowRequest(c *check.C) {
157
+
158
+	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
159
+	c.Assert(err, check.IsNil)
160
+	s.ctrl.reqRes.Allow = true
161
+	s.ctrl.resRes.Allow = true
162
+
163
+	// Ensure command successful
164
+	out, err := s.d.Cmd("run", "-d", "--name", "container1", "busybox:latest", "top")
165
+	c.Assert(err, check.IsNil)
166
+
167
+	// Extract the id of the created container
168
+	res := strings.Split(strings.TrimSpace(out), "\n")
169
+	id := res[len(res)-1]
170
+	assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
171
+	assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id))
172
+
173
+	out, err = s.d.Cmd("ps")
174
+	c.Assert(err, check.IsNil)
175
+	c.Assert(assertContainerList(out, []string{id}), check.Equals, true)
176
+	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
177
+	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
178
+}
179
+
180
+func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
181
+
182
+	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
183
+	c.Assert(err, check.IsNil)
184
+	s.ctrl.reqRes.Allow = false
185
+	s.ctrl.reqRes.Msg = unauthorizedMessage
186
+
187
+	// Ensure command is blocked
188
+	res, err := s.d.Cmd("ps")
189
+	c.Assert(err, check.NotNil)
190
+	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
191
+	c.Assert(s.ctrl.psResponseCnt, check.Equals, 0)
192
+
193
+	// Ensure unauthorized message appears in response
194
+	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: %s\n", unauthorizedMessage))
195
+}
196
+
197
+func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
198
+
199
+	err := s.d.Start("--authz-plugins=" + testAuthZPlugin)
200
+	c.Assert(err, check.IsNil)
201
+	s.ctrl.reqRes.Allow = true
202
+	s.ctrl.resRes.Allow = false
203
+	s.ctrl.resRes.Msg = unauthorizedMessage
204
+
205
+	// Ensure command is blocked
206
+	res, err := s.d.Cmd("ps")
207
+	c.Assert(err, check.NotNil)
208
+	c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
209
+	c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
210
+
211
+	// Ensure unauthorized message appears in response
212
+	c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: %s\n", unauthorizedMessage))
213
+}
214
+
215
+// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
216
+func assertURIRecorded(c *check.C, uris []string, uri string) {
217
+
218
+	found := false
219
+	for _, u := range uris {
220
+		if strings.Contains(u, uri) {
221
+			found = true
222
+		}
223
+	}
224
+	if !found {
225
+		c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
226
+	}
227
+}
... ...
@@ -133,7 +133,7 @@ func (s *DockerSuite) TestHelpTextVerify(c *check.C) {
133 133
 			// Check each line for lots of stuff
134 134
 			lines := strings.Split(out, "\n")
135 135
 			for _, line := range lines {
136
-				c.Assert(len(line), checker.LessOrEqualThan, 90, check.Commentf("Help for %q is too long:\n%s", cmd, line))
136
+				c.Assert(len(line), checker.LessOrEqualThan, 91, check.Commentf("Help for %q is too long:\n%s", cmd, line))
137 137
 
138 138
 				if scanForHome && strings.Contains(line, `"`+home) {
139 139
 					c.Fatalf("Help for %q should use ~ instead of %q on:\n%s",
140 140
new file mode 100644
... ...
@@ -0,0 +1,52 @@
0
+package authorization
1
+
2
+const (
3
+	// AuthZApiRequest is the url for daemon request authorization
4
+	AuthZApiRequest = "AuthZPlugin.AuthZReq"
5
+
6
+	// AuthZApiResponse is the url for daemon response authorization
7
+	AuthZApiResponse = "AuthZPlugin.AuthZRes"
8
+
9
+	// AuthZApiImplements is the name of the interface all AuthZ plugins implement
10
+	AuthZApiImplements = "authz"
11
+)
12
+
13
+// Request holds data required for authZ plugins
14
+type Request struct {
15
+	// User holds the user extracted by AuthN mechanism
16
+	User string `json:"User,omitempty"`
17
+
18
+	// UserAuthNMethod holds the mechanism used to extract user details (e.g., krb)
19
+	UserAuthNMethod string `json:"UserAuthNMethod,omitempty"`
20
+
21
+	// RequestMethod holds the HTTP method (GET/POST/PUT)
22
+	RequestMethod string `json:"RequestMethod,omitempty"`
23
+
24
+	// RequestUri holds the full HTTP uri (e.g., /v1.21/version)
25
+	RequestURI string `json:"RequestUri,omitempty"`
26
+
27
+	// RequestBody stores the raw request body sent to the docker daemon
28
+	RequestBody []byte `json:"RequestBody,omitempty"`
29
+
30
+	// RequestHeaders stores the raw request headers sent to the docker daemon
31
+	RequestHeaders map[string]string `json:"RequestHeaders,omitempty"`
32
+
33
+	// ResponseStatusCode stores the status code returned from docker daemon
34
+	ResponseStatusCode int `json:"ResponseStatusCode,omitempty"`
35
+
36
+	// ResponseBody stores the raw response body sent from docker daemon
37
+	ResponseBody []byte `json:"ResponseBody,omitempty"`
38
+
39
+	// ResponseHeaders stores the response headers sent to the docker daemon
40
+	ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"`
41
+}
42
+
43
+// Response represents authZ plugin response
44
+type Response struct {
45
+
46
+	// Allow indicating whether the user is allowed or not
47
+	Allow bool `json:"Allow"`
48
+
49
+	// Msg stores the authorization message
50
+	Msg string `json:"Msg,omitempty"`
51
+}
0 52
new file mode 100644
... ...
@@ -0,0 +1,83 @@
0
+# Docker Authorization Plug-in API
1
+
2
+## Introduction
3
+
4
+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. 
5
+
6
+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. 
7
+
8
+The authorization depends on the authorization effort that takes place in parallel [https://github.com/docker/docker/issues/13697]. 
9
+
10
+This is the official issue of the authorization effort: https://github.com/docker/docker/issues/14674
11
+
12
+(Here)[https://github.com/rhatdan/docker-rbac] you can find an open document that discusses a default RBAC plug-in for Docker. 
13
+
14
+## Docker daemon configuration 
15
+
16
+In order to add a single authentication plug-in or a set of such, please use the following command line argument:
17
+
18
+``` docker -d authz-plugin=authZPlugin1,authZPlugin2 ```
19
+
20
+## API
21
+
22
+The skeleton code for a typical plug-in can be found here [ADD LINK]. The plug-in must implement two AP methods:
23
+
24
+1. */AuthzPlugin.AuthZReq* - this is the _authorize request_ method that is called before executing the Docker operation. 
25
+1. */AuthzPlugin.AuthZRes* - this is the _authorize response_ method that is called before returning the response to the client. 
26
+
27
+#### /AuthzPlugin.AuthZReq
28
+
29
+**Request**:
30
+
31
+```
32
+{    
33
+    "User":              "The user identification"
34
+    "UserAuthNMethod":   "The authentication method used"
35
+    "RequestMethod":     "The HTTP method"
36
+    "RequestUri":        "The HTTP request URI"
37
+    "RequestBody":       "Byte array containing the raw HTTP request body"
38
+    "RequestHeader":     "Byte array containing the raw HTTP request header as a map[string][]string "
39
+    "RequestStatusCode": "Request status code"
40
+}
41
+```
42
+
43
+**Response**:
44
+
45
+```
46
+{    
47
+    "Allow" : "Determined whether the user is allowed or not"
48
+    "Msg":    "The authorization message"
49
+}
50
+```
51
+
52
+#### /AuthzPlugin.AuthZRes
53
+
54
+**Request**:
55
+```
56
+{
57
+    "User":              "The user identification"
58
+    "UserAuthNMethod":   "The authentication method used"
59
+    "RequestMethod":     "The HTTP method"
60
+    "RequestUri":        "The HTTP request URI"
61
+    "RequestBody":       "Byte array containing the raw HTTP request body"
62
+    "RequestHeader":     "Byte array containing the raw HTTP request header as a map[string][]string"
63
+    "RequestStatusCode": "Request status code"
64
+    "ResponseBody":      "Byte array containing the raw HTTP response body"
65
+    "ResponseHeader":    "Byte array containing the raw HTTP response header as a map[string][]string"
66
+    "ResponseStatusCode":"Response status code"
67
+}
68
+```
69
+
70
+**Response**:
71
+```
72
+{
73
+   "Allow" :               "Determined whether the user is allowed or not"
74
+   "Msg":                  "The authorization message"
75
+   "ModifiedBody":         "Byte array containing a modified body of the raw HTTP body (or nil if no changes required)"
76
+   "ModifiedHeader":       "Byte array containing a modified header of the HTTP response (or nil if no changes required)"
77
+   "ModifiedStatusCode":   "int containing the modified version of the status code (or 0 if not change is required)"
78
+}
79
+```
80
+
81
+The modified response enables the authorization plug-in to manipulate the content of the HTTP response.
82
+In case of more than one plug-in, each subsequent plug-in will received a response (optionally) modified by a previous plug-in. 
0 83
\ No newline at end of file
1 84
new file mode 100644
... ...
@@ -0,0 +1,159 @@
0
+package authorization
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"io"
6
+	"io/ioutil"
7
+	"net/http"
8
+	"strings"
9
+)
10
+
11
+// NewCtx creates new authZ context, it is used to store authorization information related to a specific docker
12
+// REST http session
13
+// A context provides two method:
14
+// Authenticate Request:
15
+// Call authZ plugins with current REST request and AuthN response
16
+// Request contains full HTTP packet sent to the docker daemon
17
+// https://docs.docker.com/reference/api/docker_remote_api/
18
+//
19
+// Authenticate Response:
20
+// Call authZ plugins with full info about current REST request, REST response and AuthN response
21
+// The response from this method may contains content that overrides the daemon response
22
+// This allows authZ plugins to filter privileged content
23
+//
24
+// If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results
25
+// For response manipulation, the response from each plugin is piped between plugins. Plugin execution order
26
+// is determined according to daemon parameters
27
+func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx {
28
+	return &Ctx{plugins: authZPlugins, user: user, userAuthNMethod: userAuthNMethod, requestMethod: requestMethod, requestURI: requestURI}
29
+}
30
+
31
+// Ctx stores a a single request-response interaction context
32
+type Ctx struct {
33
+	user            string
34
+	userAuthNMethod string
35
+	requestMethod   string
36
+	requestURI      string
37
+	plugins         []Plugin
38
+	// authReq stores the cached request object for the current transaction
39
+	authReq *Request
40
+}
41
+
42
+// AuthZRequest authorized the request to the docker daemon using authZ plugins
43
+func (a *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) (err error) {
44
+
45
+	var body []byte
46
+	if sendBody(a.requestURI, r.Header) {
47
+		var drainedBody io.ReadCloser
48
+		drainedBody, r.Body, err = drainBody(r.Body)
49
+		if err != nil {
50
+			return err
51
+		}
52
+		body, err = ioutil.ReadAll(drainedBody)
53
+		defer drainedBody.Close()
54
+
55
+		if err != nil {
56
+			return err
57
+		}
58
+	}
59
+
60
+	var h bytes.Buffer
61
+	err = r.Header.Write(&h)
62
+
63
+	if err != nil {
64
+		return err
65
+	}
66
+
67
+	a.authReq = &Request{
68
+		User:            a.user,
69
+		UserAuthNMethod: a.userAuthNMethod,
70
+		RequestMethod:   a.requestMethod,
71
+		RequestURI:      a.requestURI,
72
+		RequestBody:     body,
73
+		RequestHeaders:  headers(r.Header)}
74
+
75
+	for _, plugin := range a.plugins {
76
+
77
+		authRes, err := plugin.AuthZRequest(a.authReq)
78
+
79
+		if err != nil {
80
+			return err
81
+		}
82
+
83
+		if !authRes.Allow {
84
+			return fmt.Errorf(authRes.Msg)
85
+		}
86
+	}
87
+
88
+	return nil
89
+}
90
+
91
+// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
92
+func (a *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
93
+
94
+	a.authReq.ResponseStatusCode = rm.StatusCode()
95
+	a.authReq.ResponseHeaders = headers(rm.Header())
96
+
97
+	if sendBody(a.requestURI, rm.Header()) {
98
+		a.authReq.ResponseBody = rm.RawBody()
99
+	}
100
+
101
+	for _, plugin := range a.plugins {
102
+
103
+		authRes, err := plugin.AuthZResponse(a.authReq)
104
+
105
+		if err != nil {
106
+			return err
107
+		}
108
+
109
+		if !authRes.Allow {
110
+			return fmt.Errorf(authRes.Msg)
111
+		}
112
+	}
113
+
114
+	rm.Flush()
115
+
116
+	return nil
117
+}
118
+
119
+// drainBody dump the body, it reads the body data into memory and
120
+// see go sources /go/src/net/http/httputil/dump.go
121
+func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
122
+	var buf bytes.Buffer
123
+	if _, err = buf.ReadFrom(b); err != nil {
124
+		return nil, nil, err
125
+	}
126
+	if err = b.Close(); err != nil {
127
+		return nil, nil, err
128
+	}
129
+	return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
130
+}
131
+
132
+// sendBody returns true when request/response body should be sent to AuthZPlugin
133
+func sendBody(url string, header http.Header) bool {
134
+
135
+	// Skip body for auth endpoint
136
+	if strings.HasSuffix(url, "/auth") {
137
+		return false
138
+	}
139
+
140
+	// body is sent only for text or json messages
141
+	v := header.Get("Content-Type")
142
+	return strings.HasPrefix(v, "text/") || v == "application/json"
143
+}
144
+
145
+// headers returns flatten version of the http headers excluding authorization
146
+func headers(header http.Header) map[string]string {
147
+	v := make(map[string]string, 0)
148
+	for k, values := range header {
149
+		// Skip authorization headers
150
+		if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") {
151
+			continue
152
+		}
153
+		for _, val := range values {
154
+			v[k] = val
155
+		}
156
+	}
157
+	return v
158
+}
0 159
new file mode 100644
... ...
@@ -0,0 +1,220 @@
0
+package authorization
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"github.com/docker/docker/pkg/plugins"
6
+	"github.com/docker/docker/pkg/tlsconfig"
7
+	"github.com/gorilla/mux"
8
+	"io/ioutil"
9
+	"log"
10
+	"net"
11
+	"net/http"
12
+	"net/http/httptest"
13
+	"os"
14
+	"path"
15
+	"reflect"
16
+	"testing"
17
+)
18
+
19
+const pluginAddress = "authzplugin.sock"
20
+
21
+func TestAuthZRequestPlugin(t *testing.T) {
22
+
23
+	server := authZPluginTestServer{t: t}
24
+	go server.start()
25
+	defer server.stop()
26
+
27
+	authZPlugin := createTestPlugin(t)
28
+
29
+	request := Request{
30
+		User:           "user",
31
+		RequestBody:    []byte("sample body"),
32
+		RequestURI:     "www.authz.com",
33
+		RequestMethod:  "GET",
34
+		RequestHeaders: map[string]string{"header": "value"},
35
+	}
36
+	server.replayResponse = Response{
37
+		Allow: true,
38
+		Msg:   "Sample message",
39
+	}
40
+
41
+	actualResponse, err := authZPlugin.AuthZRequest(&request)
42
+
43
+	if err != nil {
44
+		t.Fatalf("Failed to authorize request %v", err)
45
+	}
46
+
47
+	if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
48
+		t.Fatalf("Response must be equal")
49
+	}
50
+	if !reflect.DeepEqual(request, server.recordedRequest) {
51
+		t.Fatalf("Requests must be equal")
52
+	}
53
+}
54
+
55
+func TestAuthZResponsePlugin(t *testing.T) {
56
+
57
+	server := authZPluginTestServer{t: t}
58
+	go server.start()
59
+	defer server.stop()
60
+
61
+	authZPlugin := createTestPlugin(t)
62
+
63
+	request := Request{
64
+		User:        "user",
65
+		RequestBody: []byte("sample body"),
66
+	}
67
+	server.replayResponse = Response{
68
+		Allow: true,
69
+		Msg:   "Sample message",
70
+	}
71
+
72
+	actualResponse, err := authZPlugin.AuthZResponse(&request)
73
+
74
+	if err != nil {
75
+		t.Fatalf("Failed to authorize request %v", err)
76
+	}
77
+
78
+	if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
79
+		t.Fatalf("Response must be equal")
80
+	}
81
+	if !reflect.DeepEqual(request, server.recordedRequest) {
82
+		t.Fatalf("Requests must be equal")
83
+	}
84
+}
85
+
86
+func TestResponseModifier(t *testing.T) {
87
+
88
+	r := httptest.NewRecorder()
89
+	m := NewResponseModifier(r)
90
+	m.Header().Set("h1", "v1")
91
+	m.Write([]byte("body"))
92
+	m.WriteHeader(500)
93
+
94
+	m.Flush()
95
+	if r.Header().Get("h1") != "v1" {
96
+		t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
97
+	}
98
+	if !reflect.DeepEqual(r.Body.Bytes(), []byte("body")) {
99
+		t.Fatalf("Body value must exists %s", r.Body.Bytes())
100
+	}
101
+	if r.Code != 500 {
102
+		t.Fatalf("Status code must be correct %d", r.Code)
103
+	}
104
+}
105
+
106
+func TestResponseModifierOverride(t *testing.T) {
107
+
108
+	r := httptest.NewRecorder()
109
+	m := NewResponseModifier(r)
110
+	m.Header().Set("h1", "v1")
111
+	m.Write([]byte("body"))
112
+	m.WriteHeader(500)
113
+
114
+	overrideHeader := make(http.Header)
115
+	overrideHeader.Add("h1", "v2")
116
+	overrideHeaderBytes, err := json.Marshal(overrideHeader)
117
+	if err != nil {
118
+		t.Fatalf("override header failed %v", err)
119
+	}
120
+
121
+	m.OverrideHeader(overrideHeaderBytes)
122
+	m.OverrideBody([]byte("override body"))
123
+	m.OverrideStatusCode(404)
124
+	m.Flush()
125
+	if r.Header().Get("h1") != "v2" {
126
+		t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
127
+	}
128
+	if !reflect.DeepEqual(r.Body.Bytes(), []byte("override body")) {
129
+		t.Fatalf("Body value must exists %s", r.Body.Bytes())
130
+	}
131
+	if r.Code != 404 {
132
+		t.Fatalf("Status code must be correct %d", r.Code)
133
+	}
134
+}
135
+
136
+// createTestPlugin creates a new sample authorization plugin
137
+func createTestPlugin(t *testing.T) *authorizationPlugin {
138
+	plugin := &plugins.Plugin{Name: "authz"}
139
+	var err error
140
+	pwd, err := os.Getwd()
141
+	if err != nil {
142
+		fmt.Println(err)
143
+		os.Exit(1)
144
+	}
145
+	if err != nil {
146
+		log.Fatal(err)
147
+	}
148
+
149
+	plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true})
150
+
151
+	if err != nil {
152
+		t.Fatalf("Failed to create client %v", err)
153
+	}
154
+
155
+	return &authorizationPlugin{name: "plugin", plugin: plugin}
156
+}
157
+
158
+// AuthZPluginTestServer is a simple server that implements the authZ plugin interface
159
+type authZPluginTestServer struct {
160
+	listener net.Listener
161
+	t        *testing.T
162
+	// request stores the request sent from the daemon to the plugin
163
+	recordedRequest Request
164
+	// response stores the response sent from the plugin to the daemon
165
+	replayResponse Response
166
+}
167
+
168
+// start starts the test server that implements the plugin
169
+func (t *authZPluginTestServer) start() {
170
+	r := mux.NewRouter()
171
+	os.Remove(pluginAddress)
172
+	l, err := net.ListenUnix("unix", &net.UnixAddr{Name: pluginAddress, Net: "unix"})
173
+	if err != nil {
174
+		t.t.Fatalf("Failed to listen %v", err)
175
+	}
176
+	t.listener = l
177
+
178
+	r.HandleFunc("/Plugin.Activate", t.activate)
179
+	r.HandleFunc("/"+AuthZApiRequest, t.auth)
180
+	r.HandleFunc("/"+AuthZApiResponse, t.auth)
181
+	t.listener, err = net.Listen("tcp", pluginAddress)
182
+	server := http.Server{Handler: r, Addr: pluginAddress}
183
+	server.Serve(l)
184
+}
185
+
186
+// stop stops the test server that implements the plugin
187
+func (t *authZPluginTestServer) stop() {
188
+
189
+	os.Remove(pluginAddress)
190
+
191
+	if t.listener != nil {
192
+		t.listener.Close()
193
+	}
194
+}
195
+
196
+// auth is a used to record/replay the authentication api messages
197
+func (t *authZPluginTestServer) auth(w http.ResponseWriter, r *http.Request) {
198
+
199
+	t.recordedRequest = Request{}
200
+
201
+	defer r.Body.Close()
202
+	body, err := ioutil.ReadAll(r.Body)
203
+	json.Unmarshal(body, &t.recordedRequest)
204
+	b, err := json.Marshal(t.replayResponse)
205
+	if err != nil {
206
+		log.Fatal(err)
207
+	}
208
+	w.Write(b)
209
+
210
+}
211
+
212
+func (t *authZPluginTestServer) activate(w http.ResponseWriter, r *http.Request) {
213
+
214
+	b, err := json.Marshal(plugins.Manifest{Implements: []string{AuthZApiImplements}})
215
+	if err != nil {
216
+		log.Fatal(err)
217
+	}
218
+	w.Write(b)
219
+}
0 220
new file mode 100644
... ...
@@ -0,0 +1,87 @@
0
+package authorization
1
+
2
+import (
3
+	"github.com/Sirupsen/logrus"
4
+	"github.com/docker/docker/pkg/plugins"
5
+)
6
+
7
+// Plugin allows third party plugins to authorize requests and responses
8
+// in the context of docker API
9
+type Plugin interface {
10
+
11
+	// AuthZRequest authorize the request from the client to the daemon
12
+	AuthZRequest(authReq *Request) (authRes *Response, err error)
13
+
14
+	// AuthZResponse authorize the response from the daemon to the client
15
+	AuthZResponse(authReq *Request) (authRes *Response, err error)
16
+}
17
+
18
+// NewPlugins constructs and initialize the authorization plugins based on plugin names
19
+func NewPlugins(names []string) []Plugin {
20
+	plugins := make([]Plugin, len(names))
21
+	for i, name := range names {
22
+		plugins[i] = newAuthorizationPlugin(name)
23
+	}
24
+	return plugins
25
+}
26
+
27
+// authorizationPlugin is an internal adapter to docker plugin system
28
+type authorizationPlugin struct {
29
+	plugin *plugins.Plugin
30
+	name   string
31
+}
32
+
33
+func newAuthorizationPlugin(name string) Plugin {
34
+	return &authorizationPlugin{name: name}
35
+}
36
+
37
+func (a *authorizationPlugin) AuthZRequest(authReq *Request) (authRes *Response, err error) {
38
+
39
+	logrus.Debugf("AuthZ requset using plugins %s", a.name)
40
+
41
+	err = a.initPlugin()
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+
46
+	authRes = &Response{}
47
+	err = a.plugin.Client.Call(AuthZApiRequest, authReq, authRes)
48
+
49
+	if err != nil {
50
+		return nil, err
51
+	}
52
+
53
+	return authRes, nil
54
+}
55
+
56
+func (a *authorizationPlugin) AuthZResponse(authReq *Request) (authRes *Response, err error) {
57
+
58
+	logrus.Debugf("AuthZ response using plugins %s", a.name)
59
+
60
+	err = a.initPlugin()
61
+	if err != nil {
62
+		return nil, err
63
+	}
64
+
65
+	authRes = &Response{}
66
+	err = a.plugin.Client.Call(AuthZApiResponse, authReq, authRes)
67
+
68
+	if err != nil {
69
+		return nil, err
70
+	}
71
+
72
+	return authRes, nil
73
+}
74
+
75
+// initPlugin initialize the authorization plugin if needed
76
+func (a *authorizationPlugin) initPlugin() (err error) {
77
+
78
+	// Lazy loading of plugins
79
+	if a.plugin == nil {
80
+		a.plugin, err = plugins.Get(a.name, AuthZApiImplements)
81
+		if err != nil {
82
+			return err
83
+		}
84
+	}
85
+	return nil
86
+}
0 87
new file mode 100644
... ...
@@ -0,0 +1,140 @@
0
+package authorization
1
+
2
+import (
3
+	"bufio"
4
+	"bytes"
5
+	"encoding/json"
6
+	"fmt"
7
+	"net"
8
+	"net/http"
9
+)
10
+
11
+// ResponseModifier allows authorization plugins to read and modify the content of the http.response
12
+type ResponseModifier interface {
13
+	http.ResponseWriter
14
+
15
+	// RawBody returns the current http content
16
+	RawBody() []byte
17
+
18
+	// RawHeaders returns the current content of the http headers
19
+	RawHeaders() ([]byte, error)
20
+
21
+	// StatusCode returns the current status code
22
+	StatusCode() int
23
+
24
+	// OverrideBody replace the body of the HTTP reply
25
+	OverrideBody(b []byte)
26
+
27
+	// OverrideHeader replace the headers of the HTTP reply
28
+	OverrideHeader(b []byte) error
29
+
30
+	// OverrideStatusCode replaces the status code of the HTTP reply
31
+	OverrideStatusCode(statusCode int)
32
+
33
+	// Flush flushes all data to the HTTP response
34
+	Flush() error
35
+}
36
+
37
+// NewResponseModifier creates a wrapper to an http.ResponseWriter to allow inspecting and modifying the content
38
+func NewResponseModifier(rw http.ResponseWriter) ResponseModifier {
39
+	return &responseModifier{rw: rw, header: make(http.Header)}
40
+}
41
+
42
+// responseModifier is used as an adapter to http.ResponseWriter in order to manipulate and explore
43
+// the http request/response from docker daemon
44
+type responseModifier struct {
45
+	// The original response writer
46
+	rw     http.ResponseWriter
47
+	status int
48
+	// body holds the response body
49
+	body []byte
50
+	// header holds the response header
51
+	header http.Header
52
+	// statusCode holds the response status code
53
+	statusCode int
54
+}
55
+
56
+// WriterHeader stores the http status code
57
+func (rm *responseModifier) WriteHeader(s int) {
58
+	rm.statusCode = s
59
+}
60
+
61
+// Header returns the internal http header
62
+func (rm *responseModifier) Header() http.Header {
63
+	return rm.header
64
+}
65
+
66
+// Header returns the internal http header
67
+func (rm *responseModifier) StatusCode() int {
68
+	return rm.statusCode
69
+}
70
+
71
+// Override replace the body of the HTTP reply
72
+func (rm *responseModifier) OverrideBody(b []byte) {
73
+	rm.body = b
74
+}
75
+
76
+func (rm *responseModifier) OverrideStatusCode(statusCode int) {
77
+	rm.statusCode = statusCode
78
+}
79
+
80
+// Override replace the headers of the HTTP reply
81
+func (rm *responseModifier) OverrideHeader(b []byte) error {
82
+	header := http.Header{}
83
+	err := json.Unmarshal(b, &header)
84
+
85
+	if err != nil {
86
+		return err
87
+	}
88
+	rm.header = header
89
+	return nil
90
+}
91
+
92
+// Write stores the byte array inside content
93
+func (rm *responseModifier) Write(b []byte) (int, error) {
94
+	rm.body = append(rm.body, b...)
95
+	return len(b), nil
96
+}
97
+
98
+// Body returns the response body
99
+func (rm *responseModifier) RawBody() []byte {
100
+	return rm.body
101
+}
102
+
103
+func (rm *responseModifier) RawHeaders() ([]byte, error) {
104
+	var b bytes.Buffer
105
+	err := rm.header.Write(&b)
106
+	if err != nil {
107
+		return nil, err
108
+	}
109
+	return b.Bytes(), nil
110
+}
111
+
112
+// Hijack returns the internal connection of the wrapped http.ResponseWriter
113
+func (rm *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
114
+	hijacker, ok := rm.rw.(http.Hijacker)
115
+	if !ok {
116
+		return nil, nil, fmt.Errorf("Internal reponse writer doesn't support the Hijacker interface")
117
+	}
118
+	return hijacker.Hijack()
119
+}
120
+
121
+// Flush flushes all data to the HTTP response
122
+func (rm *responseModifier) Flush() error {
123
+
124
+	// Copy the status code
125
+	if rm.statusCode > 0 {
126
+		rm.rw.WriteHeader(rm.statusCode)
127
+	}
128
+
129
+	// Copy the header
130
+	for k, vv := range rm.header {
131
+		for _, v := range vv {
132
+			rm.rw.Header().Add(k, v)
133
+		}
134
+	}
135
+
136
+	// Write body
137
+	_, err := rm.rw.Write(rm.body)
138
+	return err
139
+}