mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
3c157713b3
Currently Docker authorization framework does not use any user information, which already available in the Docker context for TLS connection. The purpose of this CR is to complete the existing authz work by adding the basic client certificate details (SUBJECT_NAME) and authentication method (TLS) to the authz request. We think this should be the default behavior when no extended authorization module is specified (currently WIP under #20883). Signed-off-by: Liron Levin <liron@twistlock.com>
406 lines
12 KiB
Go
406 lines
12 KiB
Go
// +build !windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
|
|
"bufio"
|
|
"bytes"
|
|
"os/exec"
|
|
"strconv"
|
|
"time"
|
|
|
|
"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
|
|
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 = 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) {
|
|
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
|
|
c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
|
|
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
|
|
if err := s.d.Start(
|
|
"--authorization-plugin="+testAuthZPlugin,
|
|
"--tlsverify",
|
|
"--tlscacert",
|
|
"fixtures/https/ca.pem",
|
|
"--tlscert",
|
|
"fixtures/https/server-cert.pem",
|
|
"--tlskey",
|
|
"fixtures/https/server-key.pem",
|
|
"-H", testDaemonHTTPSAddr); err != nil {
|
|
c.Fatalf("Could not start daemon with busybox: %v", err)
|
|
}
|
|
|
|
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) {
|
|
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 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
|
|
c.Assert(s.d.Start("--authorization-plugin="+testAuthZPlugin), check.IsNil)
|
|
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(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
|
|
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) {
|
|
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, ","))
|
|
}
|
|
}
|