Merge pull request #15365 from twistlock/14674-docker-authz
Docker authorization plug-in infrastructure
This commit is contained in:
commit
df5dc52c05
5 changed files with 658 additions and 0 deletions
52
authorization/api.go
Normal file
52
authorization/api.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AuthZApiRequest is the url for daemon request authorization
|
||||||
|
AuthZApiRequest = "AuthZPlugin.AuthZReq"
|
||||||
|
|
||||||
|
// AuthZApiResponse is the url for daemon response authorization
|
||||||
|
AuthZApiResponse = "AuthZPlugin.AuthZRes"
|
||||||
|
|
||||||
|
// AuthZApiImplements is the name of the interface all AuthZ plugins implement
|
||||||
|
AuthZApiImplements = "authz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Request holds data required for authZ plugins
|
||||||
|
type Request struct {
|
||||||
|
// User holds the user extracted by AuthN mechanism
|
||||||
|
User string `json:"User,omitempty"`
|
||||||
|
|
||||||
|
// UserAuthNMethod holds the mechanism used to extract user details (e.g., krb)
|
||||||
|
UserAuthNMethod string `json:"UserAuthNMethod,omitempty"`
|
||||||
|
|
||||||
|
// RequestMethod holds the HTTP method (GET/POST/PUT)
|
||||||
|
RequestMethod string `json:"RequestMethod,omitempty"`
|
||||||
|
|
||||||
|
// RequestUri holds the full HTTP uri (e.g., /v1.21/version)
|
||||||
|
RequestURI string `json:"RequestUri,omitempty"`
|
||||||
|
|
||||||
|
// RequestBody stores the raw request body sent to the docker daemon
|
||||||
|
RequestBody []byte `json:"RequestBody,omitempty"`
|
||||||
|
|
||||||
|
// RequestHeaders stores the raw request headers sent to the docker daemon
|
||||||
|
RequestHeaders map[string]string `json:"RequestHeaders,omitempty"`
|
||||||
|
|
||||||
|
// ResponseStatusCode stores the status code returned from docker daemon
|
||||||
|
ResponseStatusCode int `json:"ResponseStatusCode,omitempty"`
|
||||||
|
|
||||||
|
// ResponseBody stores the raw response body sent from docker daemon
|
||||||
|
ResponseBody []byte `json:"ResponseBody,omitempty"`
|
||||||
|
|
||||||
|
// ResponseHeaders stores the response headers sent to the docker daemon
|
||||||
|
ResponseHeaders map[string]string `json:"ResponseHeaders,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response represents authZ plugin response
|
||||||
|
type Response struct {
|
||||||
|
|
||||||
|
// Allow indicating whether the user is allowed or not
|
||||||
|
Allow bool `json:"Allow"`
|
||||||
|
|
||||||
|
// Msg stores the authorization message
|
||||||
|
Msg string `json:"Msg,omitempty"`
|
||||||
|
}
|
159
authorization/authz.go
Normal file
159
authorization/authz.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCtx creates new authZ context, it is used to store authorization information related to a specific docker
|
||||||
|
// REST http session
|
||||||
|
// A context provides two method:
|
||||||
|
// Authenticate Request:
|
||||||
|
// Call authZ plugins with current REST request and AuthN response
|
||||||
|
// Request contains full HTTP packet sent to the docker daemon
|
||||||
|
// https://docs.docker.com/reference/api/docker_remote_api/
|
||||||
|
//
|
||||||
|
// Authenticate Response:
|
||||||
|
// Call authZ plugins with full info about current REST request, REST response and AuthN response
|
||||||
|
// The response from this method may contains content that overrides the daemon response
|
||||||
|
// This allows authZ plugins to filter privileged content
|
||||||
|
//
|
||||||
|
// If multiple authZ plugins are specified, the block/allow decision is based on ANDing all plugin results
|
||||||
|
// For response manipulation, the response from each plugin is piped between plugins. Plugin execution order
|
||||||
|
// is determined according to daemon parameters
|
||||||
|
func NewCtx(authZPlugins []Plugin, user, userAuthNMethod, requestMethod, requestURI string) *Ctx {
|
||||||
|
return &Ctx{plugins: authZPlugins, user: user, userAuthNMethod: userAuthNMethod, requestMethod: requestMethod, requestURI: requestURI}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctx stores a a single request-response interaction context
|
||||||
|
type Ctx struct {
|
||||||
|
user string
|
||||||
|
userAuthNMethod string
|
||||||
|
requestMethod string
|
||||||
|
requestURI string
|
||||||
|
plugins []Plugin
|
||||||
|
// authReq stores the cached request object for the current transaction
|
||||||
|
authReq *Request
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthZRequest authorized the request to the docker daemon using authZ plugins
|
||||||
|
func (a *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) (err error) {
|
||||||
|
|
||||||
|
var body []byte
|
||||||
|
if sendBody(a.requestURI, r.Header) {
|
||||||
|
var drainedBody io.ReadCloser
|
||||||
|
drainedBody, r.Body, err = drainBody(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
body, err = ioutil.ReadAll(drainedBody)
|
||||||
|
defer drainedBody.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var h bytes.Buffer
|
||||||
|
err = r.Header.Write(&h)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.authReq = &Request{
|
||||||
|
User: a.user,
|
||||||
|
UserAuthNMethod: a.userAuthNMethod,
|
||||||
|
RequestMethod: a.requestMethod,
|
||||||
|
RequestURI: a.requestURI,
|
||||||
|
RequestBody: body,
|
||||||
|
RequestHeaders: headers(r.Header)}
|
||||||
|
|
||||||
|
for _, plugin := range a.plugins {
|
||||||
|
|
||||||
|
authRes, err := plugin.AuthZRequest(a.authReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authRes.Allow {
|
||||||
|
return fmt.Errorf(authRes.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
|
||||||
|
func (a *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
|
||||||
|
|
||||||
|
a.authReq.ResponseStatusCode = rm.StatusCode()
|
||||||
|
a.authReq.ResponseHeaders = headers(rm.Header())
|
||||||
|
|
||||||
|
if sendBody(a.requestURI, rm.Header()) {
|
||||||
|
a.authReq.ResponseBody = rm.RawBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, plugin := range a.plugins {
|
||||||
|
|
||||||
|
authRes, err := plugin.AuthZResponse(a.authReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !authRes.Allow {
|
||||||
|
return fmt.Errorf(authRes.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rm.Flush()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// drainBody dump the body, it reads the body data into memory and
|
||||||
|
// see go sources /go/src/net/http/httputil/dump.go
|
||||||
|
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if _, err = buf.ReadFrom(b); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
if err = b.Close(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendBody returns true when request/response body should be sent to AuthZPlugin
|
||||||
|
func sendBody(url string, header http.Header) bool {
|
||||||
|
|
||||||
|
// Skip body for auth endpoint
|
||||||
|
if strings.HasSuffix(url, "/auth") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// body is sent only for text or json messages
|
||||||
|
v := header.Get("Content-Type")
|
||||||
|
return strings.HasPrefix(v, "text/") || v == "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// headers returns flatten version of the http headers excluding authorization
|
||||||
|
func headers(header http.Header) map[string]string {
|
||||||
|
v := make(map[string]string, 0)
|
||||||
|
for k, values := range header {
|
||||||
|
// Skip authorization headers
|
||||||
|
if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "X-Registry-Config") || strings.EqualFold(k, "X-Registry-Auth") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, val := range values {
|
||||||
|
v[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
220
authorization/authz_test.go
Normal file
220
authorization/authz_test.go
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/docker/docker/pkg/plugins"
|
||||||
|
"github.com/docker/docker/pkg/tlsconfig"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const pluginAddress = "authzplugin.sock"
|
||||||
|
|
||||||
|
func TestAuthZRequestPlugin(t *testing.T) {
|
||||||
|
|
||||||
|
server := authZPluginTestServer{t: t}
|
||||||
|
go server.start()
|
||||||
|
defer server.stop()
|
||||||
|
|
||||||
|
authZPlugin := createTestPlugin(t)
|
||||||
|
|
||||||
|
request := Request{
|
||||||
|
User: "user",
|
||||||
|
RequestBody: []byte("sample body"),
|
||||||
|
RequestURI: "www.authz.com",
|
||||||
|
RequestMethod: "GET",
|
||||||
|
RequestHeaders: map[string]string{"header": "value"},
|
||||||
|
}
|
||||||
|
server.replayResponse = Response{
|
||||||
|
Allow: true,
|
||||||
|
Msg: "Sample message",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualResponse, err := authZPlugin.AuthZRequest(&request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to authorize request %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
|
||||||
|
t.Fatalf("Response must be equal")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(request, server.recordedRequest) {
|
||||||
|
t.Fatalf("Requests must be equal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthZResponsePlugin(t *testing.T) {
|
||||||
|
|
||||||
|
server := authZPluginTestServer{t: t}
|
||||||
|
go server.start()
|
||||||
|
defer server.stop()
|
||||||
|
|
||||||
|
authZPlugin := createTestPlugin(t)
|
||||||
|
|
||||||
|
request := Request{
|
||||||
|
User: "user",
|
||||||
|
RequestBody: []byte("sample body"),
|
||||||
|
}
|
||||||
|
server.replayResponse = Response{
|
||||||
|
Allow: true,
|
||||||
|
Msg: "Sample message",
|
||||||
|
}
|
||||||
|
|
||||||
|
actualResponse, err := authZPlugin.AuthZResponse(&request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to authorize request %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(server.replayResponse, *actualResponse) {
|
||||||
|
t.Fatalf("Response must be equal")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(request, server.recordedRequest) {
|
||||||
|
t.Fatalf("Requests must be equal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseModifier(t *testing.T) {
|
||||||
|
|
||||||
|
r := httptest.NewRecorder()
|
||||||
|
m := NewResponseModifier(r)
|
||||||
|
m.Header().Set("h1", "v1")
|
||||||
|
m.Write([]byte("body"))
|
||||||
|
m.WriteHeader(500)
|
||||||
|
|
||||||
|
m.Flush()
|
||||||
|
if r.Header().Get("h1") != "v1" {
|
||||||
|
t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r.Body.Bytes(), []byte("body")) {
|
||||||
|
t.Fatalf("Body value must exists %s", r.Body.Bytes())
|
||||||
|
}
|
||||||
|
if r.Code != 500 {
|
||||||
|
t.Fatalf("Status code must be correct %d", r.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponseModifierOverride(t *testing.T) {
|
||||||
|
|
||||||
|
r := httptest.NewRecorder()
|
||||||
|
m := NewResponseModifier(r)
|
||||||
|
m.Header().Set("h1", "v1")
|
||||||
|
m.Write([]byte("body"))
|
||||||
|
m.WriteHeader(500)
|
||||||
|
|
||||||
|
overrideHeader := make(http.Header)
|
||||||
|
overrideHeader.Add("h1", "v2")
|
||||||
|
overrideHeaderBytes, err := json.Marshal(overrideHeader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("override header failed %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.OverrideHeader(overrideHeaderBytes)
|
||||||
|
m.OverrideBody([]byte("override body"))
|
||||||
|
m.OverrideStatusCode(404)
|
||||||
|
m.Flush()
|
||||||
|
if r.Header().Get("h1") != "v2" {
|
||||||
|
t.Fatalf("Header value must exists %s", r.Header().Get("h1"))
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(r.Body.Bytes(), []byte("override body")) {
|
||||||
|
t.Fatalf("Body value must exists %s", r.Body.Bytes())
|
||||||
|
}
|
||||||
|
if r.Code != 404 {
|
||||||
|
t.Fatalf("Status code must be correct %d", r.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTestPlugin creates a new sample authorization plugin
|
||||||
|
func createTestPlugin(t *testing.T) *authorizationPlugin {
|
||||||
|
plugin := &plugins.Plugin{Name: "authz"}
|
||||||
|
var err error
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.Client, err = plugins.NewClient("unix:///"+path.Join(pwd, pluginAddress), tlsconfig.Options{InsecureSkipVerify: true})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &authorizationPlugin{name: "plugin", plugin: plugin}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthZPluginTestServer is a simple server that implements the authZ plugin interface
|
||||||
|
type authZPluginTestServer struct {
|
||||||
|
listener net.Listener
|
||||||
|
t *testing.T
|
||||||
|
// request stores the request sent from the daemon to the plugin
|
||||||
|
recordedRequest Request
|
||||||
|
// response stores the response sent from the plugin to the daemon
|
||||||
|
replayResponse Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// start starts the test server that implements the plugin
|
||||||
|
func (t *authZPluginTestServer) start() {
|
||||||
|
r := mux.NewRouter()
|
||||||
|
os.Remove(pluginAddress)
|
||||||
|
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: pluginAddress, Net: "unix"})
|
||||||
|
if err != nil {
|
||||||
|
t.t.Fatalf("Failed to listen %v", err)
|
||||||
|
}
|
||||||
|
t.listener = l
|
||||||
|
|
||||||
|
r.HandleFunc("/Plugin.Activate", t.activate)
|
||||||
|
r.HandleFunc("/"+AuthZApiRequest, t.auth)
|
||||||
|
r.HandleFunc("/"+AuthZApiResponse, t.auth)
|
||||||
|
t.listener, err = net.Listen("tcp", pluginAddress)
|
||||||
|
server := http.Server{Handler: r, Addr: pluginAddress}
|
||||||
|
server.Serve(l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop stops the test server that implements the plugin
|
||||||
|
func (t *authZPluginTestServer) stop() {
|
||||||
|
|
||||||
|
os.Remove(pluginAddress)
|
||||||
|
|
||||||
|
if t.listener != nil {
|
||||||
|
t.listener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// auth is a used to record/replay the authentication api messages
|
||||||
|
func (t *authZPluginTestServer) auth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
t.recordedRequest = Request{}
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
json.Unmarshal(body, &t.recordedRequest)
|
||||||
|
b, err := json.Marshal(t.replayResponse)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *authZPluginTestServer) activate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
b, err := json.Marshal(plugins.Manifest{Implements: []string{AuthZApiImplements}})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
w.Write(b)
|
||||||
|
}
|
87
authorization/plugin.go
Normal file
87
authorization/plugin.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/docker/pkg/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Plugin allows third party plugins to authorize requests and responses
|
||||||
|
// in the context of docker API
|
||||||
|
type Plugin interface {
|
||||||
|
|
||||||
|
// AuthZRequest authorize the request from the client to the daemon
|
||||||
|
AuthZRequest(authReq *Request) (authRes *Response, err error)
|
||||||
|
|
||||||
|
// AuthZResponse authorize the response from the daemon to the client
|
||||||
|
AuthZResponse(authReq *Request) (authRes *Response, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPlugins constructs and initialize the authorization plugins based on plugin names
|
||||||
|
func NewPlugins(names []string) []Plugin {
|
||||||
|
plugins := make([]Plugin, len(names))
|
||||||
|
for i, name := range names {
|
||||||
|
plugins[i] = newAuthorizationPlugin(name)
|
||||||
|
}
|
||||||
|
return plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorizationPlugin is an internal adapter to docker plugin system
|
||||||
|
type authorizationPlugin struct {
|
||||||
|
plugin *plugins.Plugin
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAuthorizationPlugin(name string) Plugin {
|
||||||
|
return &authorizationPlugin{name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authorizationPlugin) AuthZRequest(authReq *Request) (authRes *Response, err error) {
|
||||||
|
|
||||||
|
logrus.Debugf("AuthZ requset using plugins %s", a.name)
|
||||||
|
|
||||||
|
err = a.initPlugin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authRes = &Response{}
|
||||||
|
err = a.plugin.Client.Call(AuthZApiRequest, authReq, authRes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return authRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *authorizationPlugin) AuthZResponse(authReq *Request) (authRes *Response, err error) {
|
||||||
|
|
||||||
|
logrus.Debugf("AuthZ response using plugins %s", a.name)
|
||||||
|
|
||||||
|
err = a.initPlugin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authRes = &Response{}
|
||||||
|
err = a.plugin.Client.Call(AuthZApiResponse, authReq, authRes)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return authRes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initPlugin initialize the authorization plugin if needed
|
||||||
|
func (a *authorizationPlugin) initPlugin() (err error) {
|
||||||
|
|
||||||
|
// Lazy loading of plugins
|
||||||
|
if a.plugin == nil {
|
||||||
|
a.plugin, err = plugins.Get(a.name, AuthZApiImplements)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
140
authorization/response.go
Normal file
140
authorization/response.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
package authorization
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseModifier allows authorization plugins to read and modify the content of the http.response
|
||||||
|
type ResponseModifier interface {
|
||||||
|
http.ResponseWriter
|
||||||
|
|
||||||
|
// RawBody returns the current http content
|
||||||
|
RawBody() []byte
|
||||||
|
|
||||||
|
// RawHeaders returns the current content of the http headers
|
||||||
|
RawHeaders() ([]byte, error)
|
||||||
|
|
||||||
|
// StatusCode returns the current status code
|
||||||
|
StatusCode() int
|
||||||
|
|
||||||
|
// OverrideBody replace the body of the HTTP reply
|
||||||
|
OverrideBody(b []byte)
|
||||||
|
|
||||||
|
// OverrideHeader replace the headers of the HTTP reply
|
||||||
|
OverrideHeader(b []byte) error
|
||||||
|
|
||||||
|
// OverrideStatusCode replaces the status code of the HTTP reply
|
||||||
|
OverrideStatusCode(statusCode int)
|
||||||
|
|
||||||
|
// Flush flushes all data to the HTTP response
|
||||||
|
Flush() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResponseModifier creates a wrapper to an http.ResponseWriter to allow inspecting and modifying the content
|
||||||
|
func NewResponseModifier(rw http.ResponseWriter) ResponseModifier {
|
||||||
|
return &responseModifier{rw: rw, header: make(http.Header)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// responseModifier is used as an adapter to http.ResponseWriter in order to manipulate and explore
|
||||||
|
// the http request/response from docker daemon
|
||||||
|
type responseModifier struct {
|
||||||
|
// The original response writer
|
||||||
|
rw http.ResponseWriter
|
||||||
|
status int
|
||||||
|
// body holds the response body
|
||||||
|
body []byte
|
||||||
|
// header holds the response header
|
||||||
|
header http.Header
|
||||||
|
// statusCode holds the response status code
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriterHeader stores the http status code
|
||||||
|
func (rm *responseModifier) WriteHeader(s int) {
|
||||||
|
rm.statusCode = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns the internal http header
|
||||||
|
func (rm *responseModifier) Header() http.Header {
|
||||||
|
return rm.header
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns the internal http header
|
||||||
|
func (rm *responseModifier) StatusCode() int {
|
||||||
|
return rm.statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override replace the body of the HTTP reply
|
||||||
|
func (rm *responseModifier) OverrideBody(b []byte) {
|
||||||
|
rm.body = b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *responseModifier) OverrideStatusCode(statusCode int) {
|
||||||
|
rm.statusCode = statusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override replace the headers of the HTTP reply
|
||||||
|
func (rm *responseModifier) OverrideHeader(b []byte) error {
|
||||||
|
header := http.Header{}
|
||||||
|
err := json.Unmarshal(b, &header)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rm.header = header
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write stores the byte array inside content
|
||||||
|
func (rm *responseModifier) Write(b []byte) (int, error) {
|
||||||
|
rm.body = append(rm.body, b...)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body returns the response body
|
||||||
|
func (rm *responseModifier) RawBody() []byte {
|
||||||
|
return rm.body
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rm *responseModifier) RawHeaders() ([]byte, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
err := rm.header.Write(&b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hijack returns the internal connection of the wrapped http.ResponseWriter
|
||||||
|
func (rm *responseModifier) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||||
|
hijacker, ok := rm.rw.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, fmt.Errorf("Internal reponse writer doesn't support the Hijacker interface")
|
||||||
|
}
|
||||||
|
return hijacker.Hijack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush flushes all data to the HTTP response
|
||||||
|
func (rm *responseModifier) Flush() error {
|
||||||
|
|
||||||
|
// Copy the status code
|
||||||
|
if rm.statusCode > 0 {
|
||||||
|
rm.rw.WriteHeader(rm.statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the header
|
||||||
|
for k, vv := range rm.header {
|
||||||
|
for _, v := range vv {
|
||||||
|
rm.rw.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write body
|
||||||
|
_, err := rm.rw.Write(rm.body)
|
||||||
|
return err
|
||||||
|
}
|
Loading…
Reference in a new issue