c502fb49dc
… to make sure it doesn't fail. It also introduce StartWithError, StopWithError and RestartWithError in case we care about the error (and want the error to happen). This removes the need to check for error and make the intent more clear : I want a deamon with busybox loaded on it — if an error occur it should fail the test, but it's not the test code that has the responsability to check that. Signed-off-by: Vincent Demeester <vincent@sbr.pm>
475 lines
15 KiB
Go
475 lines
15 KiB
Go
// +build !windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"bufio"
|
|
"bytes"
|
|
"os/exec"
|
|
"strconv"
|
|
"time"
|
|
|
|
"net"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
|
|
"github.com/docker/docker/integration-cli/daemon"
|
|
"github.com/docker/docker/pkg/authorization"
|
|
"github.com/docker/docker/pkg/integration/checker"
|
|
"github.com/docker/docker/pkg/plugins"
|
|
"github.com/go-check/check"
|
|
)
|
|
|
|
const (
|
|
testAuthZPlugin = "authzplugin"
|
|
unauthorizedMessage = "User unauthorized authz plugin"
|
|
errorMessage = "something went wrong..."
|
|
containerListAPI = "/containers/json"
|
|
)
|
|
|
|
var (
|
|
alwaysAllowed = []string{"/_ping", "/info"}
|
|
)
|
|
|
|
func init() {
|
|
check.Suite(&DockerAuthzSuite{
|
|
ds: &DockerSuite{},
|
|
})
|
|
}
|
|
|
|
type DockerAuthzSuite struct {
|
|
server *httptest.Server
|
|
ds *DockerSuite
|
|
d *daemon.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
|
|
reqUser string
|
|
resUser string
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) SetUpTest(c *check.C) {
|
|
s.d = daemon.New(c, dockerBinary, dockerdBinary, daemon.Config{
|
|
Experimental: experimentalDaemon,
|
|
})
|
|
s.ctrl = &authorizationController{}
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) TearDownTest(c *check.C) {
|
|
if s.d != nil {
|
|
s.d.Stop(c)
|
|
s.ds.TearDownTest(c)
|
|
s.ctrl = nil
|
|
}
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) SetUpSuite(c *check.C) {
|
|
mux := http.NewServeMux()
|
|
s.server = httptest.NewServer(mux)
|
|
|
|
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) {
|
|
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)
|
|
|
|
reqRes := s.ctrl.reqRes
|
|
if isAllowed(authReq.RequestURI) {
|
|
reqRes = authorization.Response{Allow: true}
|
|
}
|
|
if reqRes.Err != "" {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
b, err := json.Marshal(reqRes)
|
|
c.Assert(err, check.IsNil)
|
|
s.ctrl.reqUser = authReq.User
|
|
w.Write(b)
|
|
})
|
|
|
|
mux.HandleFunc("/AuthZPlugin.AuthZRes", func(w http.ResponseWriter, r *http.Request) {
|
|
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++
|
|
}
|
|
resRes := s.ctrl.resRes
|
|
if isAllowed(authReq.RequestURI) {
|
|
resRes = authorization.Response{Allow: true}
|
|
}
|
|
if resRes.Err != "" {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
b, err := json.Marshal(resRes)
|
|
c.Assert(err, check.IsNil)
|
|
s.ctrl.resUser = authReq.User
|
|
w.Write(b)
|
|
})
|
|
|
|
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)
|
|
}
|
|
|
|
// check for always allowed endpoints to not inhibit test framework functions
|
|
func isAllowed(reqURI string) bool {
|
|
for _, endpoint := range alwaysAllowed {
|
|
if strings.HasSuffix(reqURI, endpoint) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
s.ctrl.reqRes.Allow = true
|
|
s.ctrl.resRes.Allow = true
|
|
c.Assert(s.d.LoadBusybox(), check.IsNil)
|
|
|
|
// 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) TestAuthZPluginTls(c *check.C) {
|
|
|
|
const testDaemonHTTPSAddr = "tcp://localhost:4271"
|
|
// start the daemon and load busybox, --net=none build fails otherwise
|
|
// cause it needs to pull busybox
|
|
s.d.Start(c,
|
|
"--authorization-plugin="+testAuthZPlugin,
|
|
"--tlsverify",
|
|
"--tlscacert",
|
|
"fixtures/https/ca.pem",
|
|
"--tlscert",
|
|
"fixtures/https/server-cert.pem",
|
|
"--tlskey",
|
|
"fixtures/https/server-key.pem",
|
|
"-H", testDaemonHTTPSAddr)
|
|
|
|
s.ctrl.reqRes.Allow = true
|
|
s.ctrl.resRes.Allow = true
|
|
|
|
out, _ := dockerCmd(
|
|
c,
|
|
"--tlsverify",
|
|
"--tlscacert", "fixtures/https/ca.pem",
|
|
"--tlscert", "fixtures/https/client-cert.pem",
|
|
"--tlskey", "fixtures/https/client-key.pem",
|
|
"-H",
|
|
testDaemonHTTPSAddr,
|
|
"version",
|
|
)
|
|
if !strings.Contains(out, "Server") {
|
|
c.Fatalf("docker version should return information of server side")
|
|
}
|
|
|
|
c.Assert(s.ctrl.reqUser, check.Equals, "client")
|
|
c.Assert(s.ctrl.resUser, check.Equals, "client")
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginDenyRequest(c *check.C) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
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))
|
|
}
|
|
|
|
// TestAuthZPluginAPIDenyResponse validates that when authorization plugin deny the request, the status code is forbidden
|
|
func (s *DockerAuthzSuite) TestAuthZPluginAPIDenyResponse(c *check.C) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
s.ctrl.reqRes.Allow = false
|
|
s.ctrl.resRes.Msg = unauthorizedMessage
|
|
|
|
daemonURL, err := url.Parse(s.d.Sock())
|
|
|
|
conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
|
|
c.Assert(err, check.IsNil)
|
|
client := httputil.NewClientConn(conn, nil)
|
|
req, err := http.NewRequest("GET", "/version", nil)
|
|
c.Assert(err, check.IsNil)
|
|
resp, err := client.Do(req)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
c.Assert(resp.StatusCode, checker.Equals, http.StatusForbidden)
|
|
c.Assert(err, checker.IsNil)
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginDenyResponse(c *check.C) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
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 propagates correctly after request pass through by the authorization plugin
|
|
func (s *DockerAuthzSuite) TestAuthZPluginAllowEventStream(c *check.C) {
|
|
testRequires(c, DaemonIsLinux)
|
|
|
|
// start the daemon and load busybox to avoid pulling busybox from Docker Hub
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
s.ctrl.reqRes.Allow = true
|
|
s.ctrl.resRes.Allow = true
|
|
c.Assert(s.d.LoadBusybox(), check.IsNil)
|
|
|
|
startTime := strconv.FormatInt(daemonTime(c).Unix(), 10)
|
|
// Add another command to to enable event pipelining
|
|
eventsCmd := exec.Command(dockerBinary, "--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
|
|
out, err := s.d.Cmd("run", "-d", "busybox", "top")
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
containerID := strings.TrimSpace(out)
|
|
c.Assert(s.d.WaitRun(containerID), checker.IsNil)
|
|
|
|
events := map[string]chan bool{
|
|
"create": make(chan bool, 1),
|
|
"start": make(chan bool, 1),
|
|
}
|
|
|
|
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(30 * 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) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
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) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin)
|
|
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) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
|
|
|
|
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)
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginEnsureLoadImportWorking(c *check.C) {
|
|
s.d.Start(c, "--authorization-plugin="+testAuthZPlugin, "--authorization-plugin="+testAuthZPlugin)
|
|
s.ctrl.reqRes.Allow = true
|
|
s.ctrl.resRes.Allow = true
|
|
c.Assert(s.d.LoadBusybox(), check.IsNil)
|
|
|
|
tmp, err := ioutil.TempDir("", "test-authz-load-import")
|
|
c.Assert(err, check.IsNil)
|
|
defer os.RemoveAll(tmp)
|
|
|
|
savedImagePath := filepath.Join(tmp, "save.tar")
|
|
|
|
out, err := s.d.Cmd("save", "-o", savedImagePath, "busybox")
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
out, err = s.d.Cmd("load", "--input", savedImagePath)
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
|
|
exportedImagePath := filepath.Join(tmp, "export.tar")
|
|
|
|
out, err = s.d.Cmd("run", "-d", "--name", "testexport", "busybox")
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
out, err = s.d.Cmd("export", "-o", exportedImagePath, "testexport")
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
out, err = s.d.Cmd("import", exportedImagePath)
|
|
c.Assert(err, check.IsNil, check.Commentf(out))
|
|
}
|
|
|
|
func (s *DockerAuthzSuite) TestAuthZPluginHeader(c *check.C) {
|
|
s.d.Start(c, "--debug", "--authorization-plugin="+testAuthZPlugin)
|
|
s.ctrl.reqRes.Allow = true
|
|
s.ctrl.resRes.Allow = true
|
|
c.Assert(s.d.LoadBusybox(), check.IsNil)
|
|
|
|
daemonURL, err := url.Parse(s.d.Sock())
|
|
|
|
conn, err := net.DialTimeout(daemonURL.Scheme, daemonURL.Path, time.Second*10)
|
|
c.Assert(err, check.IsNil)
|
|
client := httputil.NewClientConn(conn, nil)
|
|
req, err := http.NewRequest("GET", "/version", nil)
|
|
c.Assert(err, check.IsNil)
|
|
resp, err := client.Do(req)
|
|
|
|
c.Assert(err, check.IsNil)
|
|
c.Assert(resp.Header["Content-Type"][0], checker.Equals, "application/json")
|
|
}
|
|
|
|
// 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, ","))
|
|
}
|
|
}
|