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
... | ... |
@@ -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 |
+} |