2015-11-12 06:06:47 -05:00
|
|
|
// +build !windows
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"os"
|
|
|
|
"strings"
|
2015-12-16 10:35:35 -05:00
|
|
|
|
|
|
|
"github.com/docker/docker/pkg/authorization"
|
|
|
|
"github.com/docker/docker/pkg/integration/checker"
|
|
|
|
"github.com/docker/docker/pkg/plugins"
|
|
|
|
"github.com/go-check/check"
|
2015-11-12 06:06:47 -05:00
|
|
|
)
|
|
|
|
|
2015-12-15 03:49:18 -05:00
|
|
|
const (
|
|
|
|
testAuthZPlugin = "authzplugin"
|
|
|
|
unauthorizedMessage = "User unauthorized authz plugin"
|
|
|
|
errorMessage = "something went wrong..."
|
|
|
|
containerListAPI = "/containers/json"
|
|
|
|
)
|
2015-11-12 06:06:47 -05:00
|
|
|
|
|
|
|
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) {
|
2015-12-15 03:49:18 -05:00
|
|
|
if s.ctrl.reqRes.Err != "" {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
2015-11-12 06:06:47 -05:00
|
|
|
b, err := json.Marshal(s.ctrl.reqRes)
|
|
|
|
c.Assert(err, check.IsNil)
|
2015-12-15 03:49:18 -05:00
|
|
|
w.Write(b)
|
2015-11-12 06:06:47 -05:00
|
|
|
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) {
|
2015-12-15 03:49:18 -05:00
|
|
|
if s.ctrl.resRes.Err != "" {
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
}
|
2015-11-12 06:06:47 -05:00
|
|
|
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) {
|
2015-12-24 10:18:23 -05:00
|
|
|
// start the daemon and load busybox, --net=none build fails otherwise
|
|
|
|
// cause it needs to pull busybox
|
|
|
|
c.Assert(s.d.StartWithBusybox(), check.IsNil)
|
|
|
|
// restart the daemon and enable the plugin, otherwise busybox loading
|
|
|
|
// is blocked by the plugin itself
|
2016-01-12 19:38:18 -05:00
|
|
|
c.Assert(s.d.Restart("--authorization-plugin="+testAuthZPlugin), check.IsNil)
|
2015-12-24 10:18:23 -05:00
|
|
|
|
2015-11-12 06:06:47 -05:00
|
|
|
s.ctrl.reqRes.Allow = true
|
|
|
|
s.ctrl.resRes.Allow = true
|
|
|
|
|
|
|
|
// Ensure command successful
|
2015-12-24 10:18:23 -05:00
|
|
|
out, err := s.d.Cmd("run", "-d", "busybox", "top")
|
2015-11-12 06:06:47 -05:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
|
2015-12-24 10:18:23 -05:00
|
|
|
id := strings.TrimSpace(out)
|
2015-11-12 06:06:47 -05:00
|
|
|
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) {
|
2016-01-12 19:38:18 -05:00
|
|
|
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
|
2015-11-12 06:06:47 -05:00
|
|
|
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
|
2015-12-18 06:34:19 -05:00
|
|
|
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
|
2015-11-12 06:06:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
|
2016-01-12 19:38:18 -05:00
|
|
|
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
|
2015-11-12 06:06:47 -05:00
|
|
|
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
|
2015-12-18 06:34:19 -05:00
|
|
|
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
|
2015-11-12 06:06:47 -05:00
|
|
|
}
|
|
|
|
|
2015-12-15 03:49:18 -05:00
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) {
|
2016-01-12 19:38:18 -05:00
|
|
|
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
|
2015-12-15 03:49:18 -05:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
s.ctrl.reqRes.Allow = true
|
|
|
|
s.ctrl.resRes.Err = errorMessage
|
|
|
|
|
|
|
|
// Ensure command is blocked
|
|
|
|
res, err := s.d.Cmd("ps")
|
|
|
|
c.Assert(err, check.NotNil)
|
|
|
|
|
2015-12-18 06:34:19 -05:00
|
|
|
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage))
|
2015-12-15 03:49:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) {
|
2016-01-12 19:38:18 -05:00
|
|
|
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
|
2015-12-15 03:49:18 -05:00
|
|
|
c.Assert(err, check.IsNil)
|
|
|
|
s.ctrl.reqRes.Err = errorMessage
|
|
|
|
|
|
|
|
// Ensure command is blocked
|
|
|
|
res, err := s.d.Cmd("ps")
|
|
|
|
c.Assert(err, check.NotNil)
|
|
|
|
|
2015-12-18 06:34:19 -05:00
|
|
|
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage))
|
2015-12-15 03:49:18 -05:00
|
|
|
}
|
|
|
|
|
2015-12-20 13:44:01 -05:00
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) {
|
2016-01-12 19:38:18 -05:00
|
|
|
c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil)
|
2015-12-20 13:44:01 -05:00
|
|
|
|
|
|
|
s.ctrl.reqRes.Allow = true
|
|
|
|
s.ctrl.resRes.Allow = true
|
|
|
|
|
|
|
|
out, err := s.d.Cmd("ps")
|
|
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
|
|
|
|
|
|
// assert plugin is only called once..
|
|
|
|
c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
|
|
|
|
c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
|
|
|
|
}
|
|
|
|
|
2015-11-12 06:06:47 -05:00
|
|
|
// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
|
|
|
|
func assertURIRecorded(c *check.C, uris []string, uri string) {
|
2015-12-20 13:44:01 -05:00
|
|
|
var found bool
|
2015-11-12 06:06:47 -05:00
|
|
|
for _, u := range uris {
|
|
|
|
if strings.Contains(u, uri) {
|
|
|
|
found = true
|
2015-12-20 13:44:01 -05:00
|
|
|
break
|
2015-11-12 06:06:47 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if !found {
|
|
|
|
c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
|
|
|
|
}
|
|
|
|
}
|