1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00
moby--moby/integration-cli/docker_cli_authz_unix_test.go
Liron Levin 5ffc810df2 Fix 19575: Docker events doesn't work with authorization plugin
To support the requirement of blocking the request after the daemon
responded the authorization plugin use a `response recorder` that replay
the response after the flow ends.

This commit adds support for commands that hijack the connection and
flushes data via the http.Flusher interface. This resolves the error
with the event endpoint.

Signed-off-by: Liron Levin <liron@twistlock.com>
2016-02-05 22:30:01 +02:00

345 lines
11 KiB
Go

// +build !windows
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"bufio"
"bytes"
"github.com/docker/docker/pkg/authorization"
"github.com/docker/docker/pkg/integration/checker"
"github.com/docker/docker/pkg/plugins"
"github.com/go-check/check"
"os/exec"
"strconv"
"time"
)
const (
testAuthZPlugin = "authzplugin"
unauthorizedMessage = "User unauthorized authz plugin"
errorMessage = "something went wrong..."
containerListAPI = "/containers/json"
)
func init() {
check.Suite(&DockerAuthzSuite{
ds: &DockerSuite{},
})
}
type DockerAuthzSuite struct {
server *httptest.Server
ds *DockerSuite
d *Daemon
ctrl *authorizationController
}
type authorizationController struct {
reqRes authorization.Response // reqRes holds the plugin response to the initial client request
resRes authorization.Response // resRes holds the plugin response to the daemon response
psRequestCnt int // psRequestCnt counts the number of calls to list container request api
psResponseCnt int // psResponseCnt counts the number of calls to list containers response API
requestsURIs []string // requestsURIs stores all request URIs that are sent to the authorization controller
}
func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
s.d = NewDaemon(c)
s.ctrl = &authorizationController{}
}
func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
s.d.Stop()
s.ds.TearDownTest(c)
s.ctrl = nil
}
func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
mux := http.NewServeMux()
s.server = httptest.NewServer(mux)
c.Assert(s.server, check.NotNil, check.Commentf("Failed to start a HTTP Server"))
mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) {
b, err := json.Marshal(plugins.Manifest{Implements: []string{authorization.AuthZApiImplements}})
c.Assert(err, check.IsNil)
w.Write(b)
})
mux.HandleFunc("/AuthZPlugin.AuthZReq", func(w http.ResponseWriter, r *http.Request) {
if s.ctrl.reqRes.Err != "" {
w.WriteHeader(http.StatusInternalServerError)
}
b, err := json.Marshal(s.ctrl.reqRes)
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.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) {
if s.ctrl.resRes.Err != "" {
w.WriteHeader(http.StatusInternalServerError)
}
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) {
// 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
c.Assert(s.d.Restart("--authorization-plugin="+testAuthZPlugin), check.IsNil)
s.ctrl.reqRes.Allow = true
s.ctrl.resRes.Allow = true
// Ensure command successful
out, err := s.d.Cmd("run", "-d", "busybox", "top")
c.Assert(err, check.IsNil)
id := strings.TrimSpace(out)
assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", id))
out, err = s.d.Cmd("ps")
c.Assert(err, check.IsNil)
c.Assert(assertContainerList(out, []string{id}), check.Equals, true)
c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
}
func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
c.Assert(err, check.IsNil)
s.ctrl.reqRes.Allow = false
s.ctrl.reqRes.Msg = unauthorizedMessage
// Ensure command is blocked
res, err := s.d.Cmd("ps")
c.Assert(err, check.NotNil)
c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
c.Assert(s.ctrl.psResponseCnt, check.Equals, 0)
// Ensure unauthorized message appears in response
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
}
func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
c.Assert(err, check.IsNil)
s.ctrl.reqRes.Allow = true
s.ctrl.resRes.Allow = false
s.ctrl.resRes.Msg = unauthorizedMessage
// Ensure command is blocked
res, err := s.d.Cmd("ps")
c.Assert(err, check.NotNil)
c.Assert(s.ctrl.psRequestCnt, check.Equals, 1)
c.Assert(s.ctrl.psResponseCnt, check.Equals, 1)
// Ensure unauthorized message appears in response
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: authorization denied by plugin %s: %s\n", testAuthZPlugin, unauthorizedMessage))
}
// TestAuthZPluginAllowEventStream verifies event stream propogates correctly after request pass through by the authorization plugin
func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) {
testRequires(c, DaemonIsLinux)
// Start the authorization plugin
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
c.Assert(err, check.IsNil)
s.ctrl.reqRes.Allow = true
s.ctrl.resRes.Allow = true
startTime := strconv.FormatInt(daemonTime(c).Unix(), 10)
// Add another command to to enable event pipelining
eventsCmd := exec.Command(s.d.cmd.Path, "--host", s.d.sock(), "events", "--since", startTime)
stdout, err := eventsCmd.StdoutPipe()
if err != nil {
c.Assert(err, check.IsNil)
}
observer := eventObserver{
buffer: new(bytes.Buffer),
command: eventsCmd,
scanner: bufio.NewScanner(stdout),
startTime: startTime,
}
err = observer.Start()
c.Assert(err, checker.IsNil)
defer observer.Stop()
// Create a container and wait for the creation events
_, err = s.d.Cmd("pull", "busybox")
c.Assert(err, check.IsNil)
out, err := s.d.Cmd("run", "-d", "busybox", "top")
c.Assert(err, check.IsNil)
containerID := strings.TrimSpace(out)
events := map[string]chan bool{
"create": make(chan bool),
"start": make(chan bool),
}
matcher := matchEventLine(containerID, "container", events)
processor := processEventMatch(events)
go observer.Match(matcher, processor)
// Ensure all events are received
for event, eventChannel := range events {
select {
case <-time.After(5 * time.Second):
// Fail the test
observer.CheckEventError(c, containerID, event, matcher)
c.FailNow()
case <-eventChannel:
// Ignore, event received
}
}
// Ensure both events and container endpoints are passed to the authorization plugin
assertURIRecorded(c, s.ctrl.requestsURIs, "/events")
assertURIRecorded(c, s.ctrl.requestsURIs, "/containers/create")
assertURIRecorded(c, s.ctrl.requestsURIs, fmt.Sprintf("/containers/%s/start", containerID))
}
func (s *DockerAuthzSuite) TestAuthZPluginErrorResponse(c *check.C) {
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
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)
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiResponse, errorMessage))
}
func (s *DockerAuthzSuite) TestAuthZPluginErrorRequest(c *check.C) {
err := s.d.Start("--authorization-plugin=" + testAuthZPlugin)
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)
c.Assert(res, check.Equals, fmt.Sprintf("Error response from daemon: plugin %s failed with error: %s: %s\n", testAuthZPlugin, authorization.AuthZApiRequest, errorMessage))
}
func (s *DockerAuthzSuite) TestAuthZPluginEnsureNoDuplicatePluginRegistration(c *check.C) {
c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin), check.IsNil)
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)
}
// assertURIRecorded verifies that the given URI was sent and recorded in the authz plugin
func assertURIRecorded(c *check.C, uris []string, uri string) {
var found bool
for _, u := range uris {
if strings.Contains(u, uri) {
found = true
break
}
}
if !found {
c.Fatalf("Expected to find URI '%s', recorded uris '%s'", uri, strings.Join(uris, ","))
}
}