Initial commit

This commit is contained in:
Ria Bhatia
2017-12-04 13:32:57 -06:00
committed by Erik St. Martin
commit 0075e5b0f3
9056 changed files with 2523100 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
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"`
// Err stores a message in case there's an error
Err string `json:"Err,omitempty"`
}

View File

@@ -0,0 +1,168 @@
package authorization
import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"strings"
"github.com/Sirupsen/logrus"
"github.com/hyperhq/hypercli/pkg/ioutils"
)
const maxBodySize = 1048576 // 1MB
// 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 (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error {
var body []byte
if sendBody(ctx.requestURI, r.Header) {
if r.ContentLength < maxBodySize {
var err error
body, r.Body, err = drainBody(r.Body)
if err != nil {
return err
}
}
}
var h bytes.Buffer
if err := r.Header.Write(&h); err != nil {
return err
}
ctx.authReq = &Request{
User: ctx.user,
UserAuthNMethod: ctx.userAuthNMethod,
RequestMethod: ctx.requestMethod,
RequestURI: ctx.requestURI,
RequestBody: body,
RequestHeaders: headers(r.Header),
}
for _, plugin := range ctx.plugins {
logrus.Debugf("AuthZ request using plugin %s", plugin.Name())
authRes, err := plugin.AuthZRequest(ctx.authReq)
if err != nil {
return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
}
if !authRes.Allow {
return fmt.Errorf("authorization denied by plugin %s: %s", plugin.Name(), authRes.Msg)
}
}
return nil
}
// AuthZResponse authorized and manipulates the response from docker daemon using authZ plugins
func (ctx *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error {
ctx.authReq.ResponseStatusCode = rm.StatusCode()
ctx.authReq.ResponseHeaders = headers(rm.Header())
if sendBody(ctx.requestURI, rm.Header()) {
ctx.authReq.ResponseBody = rm.RawBody()
}
for _, plugin := range ctx.plugins {
logrus.Debugf("AuthZ response using plugin %s", plugin.Name())
authRes, err := plugin.AuthZResponse(ctx.authReq)
if err != nil {
return fmt.Errorf("plugin %s failed with error: %s", plugin.Name(), err)
}
if !authRes.Allow {
return fmt.Errorf("authorization denied by plugin %s: %s", plugin.Name(), 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(body io.ReadCloser) ([]byte, io.ReadCloser, error) {
bufReader := bufio.NewReaderSize(body, maxBodySize)
newBody := ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() })
data, err := bufReader.Peek(maxBodySize)
if err != io.EOF {
// This means the request is larger than our max
if err == bufio.ErrBufferFull {
return nil, newBody, nil
}
// This means we had an error reading
return nil, nil, err
}
return data, newBody, 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
}

View File

@@ -0,0 +1,233 @@
package authorization
import (
"encoding/json"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"os"
"path"
"reflect"
"testing"
"github.com/hyperhq/hypercli/pkg/plugins"
"github.com/docker/go-connections/tlsconfig"
"github.com/gorilla/mux"
)
const pluginAddress = "authzplugin.sock"
func TestAuthZRequestPluginError(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{
Err: "an error",
}
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 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"}
pwd, err := os.Getwd()
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)
}

View File

@@ -0,0 +1,83 @@
package authorization
import "github.com/hyperhq/hypercli/pkg/plugins"
// Plugin allows third party plugins to authorize requests and responses
// in the context of docker API
type Plugin interface {
// Name returns the registered plugin name
Name() string
// AuthZRequest authorize the request from the client to the daemon
AuthZRequest(*Request) (*Response, error)
// AuthZResponse authorize the response from the daemon to the client
AuthZResponse(*Request) (*Response, error)
}
// NewPlugins constructs and initialize the authorization plugins based on plugin names
func NewPlugins(names []string) []Plugin {
plugins := []Plugin{}
pluginsMap := make(map[string]struct{})
for _, name := range names {
if _, ok := pluginsMap[name]; ok {
continue
}
pluginsMap[name] = struct{}{}
plugins = append(plugins, 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) Name() string {
return a.name
}
func (a *authorizationPlugin) AuthZRequest(authReq *Request) (*Response, error) {
if err := a.initPlugin(); err != nil {
return nil, err
}
authRes := &Response{}
if err := a.plugin.Client.Call(AuthZApiRequest, authReq, authRes); err != nil {
return nil, err
}
return authRes, nil
}
func (a *authorizationPlugin) AuthZResponse(authReq *Request) (*Response, error) {
if err := a.initPlugin(); err != nil {
return nil, err
}
authRes := &Response{}
if err := a.plugin.Client.Call(AuthZApiResponse, authReq, authRes); err != nil {
return nil, err
}
return authRes, nil
}
// initPlugin initialize the authorization plugin if needed
func (a *authorizationPlugin) initPlugin() error {
// Lazy loading of plugins
if a.plugin == nil {
var err error
a.plugin, err = plugins.Get(a.name, AuthZApiImplements)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,136 @@
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{}
if err := json.Unmarshal(b, &header); 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
if err := rm.header.Write(&b); 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
}