github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/apiserver/httpcontext.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package apiserver
     5  
     6  import (
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names"
    16  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    17  
    18  	"github.com/juju/juju/apiserver/common"
    19  	"github.com/juju/juju/apiserver/params"
    20  	"github.com/juju/juju/state"
    21  )
    22  
    23  // httpContext provides context for HTTP handlers.
    24  type httpContext struct {
    25  	// strictValidation means that empty modelUUID values are not valid.
    26  	strictValidation bool
    27  	// controllerModelOnly only validates the controller model.
    28  	controllerModelOnly bool
    29  	// srv holds the API server instance.
    30  	srv *Server
    31  }
    32  
    33  // stateForRequestUnauthenticated returns a state instance appropriate for
    34  // using for the model implicit in the given request
    35  // without checking any authentication information.
    36  func (ctxt *httpContext) stateForRequestUnauthenticated(r *http.Request) (*state.State, error) {
    37  	modelUUID, err := validateModelUUID(validateArgs{
    38  		statePool:           ctxt.srv.statePool,
    39  		modelUUID:           r.URL.Query().Get(":modeluuid"),
    40  		strict:              ctxt.strictValidation,
    41  		controllerModelOnly: ctxt.controllerModelOnly,
    42  	})
    43  	if err != nil {
    44  		return nil, errors.Trace(err)
    45  	}
    46  	st, err := ctxt.srv.statePool.Get(modelUUID)
    47  	if err != nil {
    48  		return nil, errors.Trace(err)
    49  	}
    50  	return st, nil
    51  }
    52  
    53  // stateForRequestAuthenticated returns a state instance appropriate for
    54  // using for the model implicit in the given request.
    55  // It also returns the authenticated entity.
    56  func (ctxt *httpContext) stateForRequestAuthenticated(r *http.Request) (*state.State, state.Entity, error) {
    57  	st, err := ctxt.stateForRequestUnauthenticated(r)
    58  	if err != nil {
    59  		return nil, nil, errors.Trace(err)
    60  	}
    61  	req, err := ctxt.loginRequest(r)
    62  	if err != nil {
    63  		return nil, nil, errors.NewUnauthorized(err, "")
    64  	}
    65  	entity, _, err := checkCreds(st, req, true, ctxt.srv.authCtxt)
    66  	if err != nil {
    67  		// All errors other than a macaroon-discharge error count as
    68  		// unauthorized at this point.
    69  		if !common.IsDischargeRequiredError(err) {
    70  			err = errors.NewUnauthorized(err, "")
    71  		}
    72  		return nil, nil, errors.Trace(err)
    73  	}
    74  	return st, entity, nil
    75  }
    76  
    77  // stateForRequestAuthenticatedUser is like stateForRequestAuthenticated
    78  // except that it also verifies that the authenticated entity is a user.
    79  func (ctxt *httpContext) stateForRequestAuthenticatedUser(r *http.Request) (*state.State, state.Entity, error) {
    80  	st, entity, err := ctxt.stateForRequestAuthenticated(r)
    81  	if err != nil {
    82  		return nil, nil, errors.Trace(err)
    83  	}
    84  	switch entity.Tag().(type) {
    85  	case names.UserTag:
    86  		return st, entity, nil
    87  	default:
    88  		return nil, nil, errors.Trace(common.ErrBadCreds)
    89  	}
    90  }
    91  
    92  // stateForRequestAuthenticatedUser is like stateForRequestAuthenticated
    93  // except that it also verifies that the authenticated entity is a user.
    94  func (ctxt *httpContext) stateForRequestAuthenticatedAgent(r *http.Request) (*state.State, state.Entity, error) {
    95  	st, entity, err := ctxt.stateForRequestAuthenticated(r)
    96  	if err != nil {
    97  		return nil, nil, errors.Trace(err)
    98  	}
    99  	switch entity.Tag().(type) {
   100  	case names.MachineTag, names.UnitTag:
   101  		return st, entity, nil
   102  	default:
   103  		logger.Errorf("attempt to log in as an agent by %v", entity.Tag())
   104  		return nil, nil, errors.Trace(common.ErrBadCreds)
   105  	}
   106  }
   107  
   108  // loginRequest forms a LoginRequest from the information
   109  // in the given HTTP request.
   110  func (ctxt *httpContext) loginRequest(r *http.Request) (params.LoginRequest, error) {
   111  	authHeader := r.Header.Get("Authorization")
   112  	if authHeader == "" {
   113  		// No authorization header implies an attempt
   114  		// to login with external user macaroon authentication.
   115  		return params.LoginRequest{
   116  			Macaroons: httpbakery.RequestMacaroons(r),
   117  		}, nil
   118  	}
   119  	parts := strings.Fields(authHeader)
   120  	if len(parts) != 2 || parts[0] != "Basic" {
   121  		// Invalid header format or no header provided.
   122  		return params.LoginRequest{}, errors.New("invalid request format")
   123  	}
   124  	// Challenge is a base64-encoded "tag:pass" string.
   125  	// See RFC 2617, Section 2.
   126  	challenge, err := base64.StdEncoding.DecodeString(parts[1])
   127  	if err != nil {
   128  		return params.LoginRequest{}, errors.New("invalid request format")
   129  	}
   130  	tagPass := strings.SplitN(string(challenge), ":", 2)
   131  	if len(tagPass) != 2 {
   132  		return params.LoginRequest{}, errors.New("invalid request format")
   133  	}
   134  	// Ensure that a sensible tag was passed.
   135  	_, err = names.ParseTag(tagPass[0])
   136  	if err != nil {
   137  		return params.LoginRequest{}, errors.Trace(common.ErrBadCreds)
   138  	}
   139  	return params.LoginRequest{
   140  		AuthTag:     tagPass[0],
   141  		Credentials: tagPass[1],
   142  		Macaroons:   httpbakery.RequestMacaroons(r),
   143  		Nonce:       r.Header.Get(params.MachineNonceHeader),
   144  	}, nil
   145  }
   146  
   147  // stop returns a channel which will be closed when a handler should
   148  // exit.
   149  func (ctxt *httpContext) stop() <-chan struct{} {
   150  	return ctxt.srv.tomb.Dying()
   151  }
   152  
   153  // sendJSON writes a JSON-encoded response value
   154  // to the given writer along with a trailing newline.
   155  func sendJSON(w io.Writer, response interface{}) {
   156  	body, err := json.Marshal(response)
   157  	if err != nil {
   158  		logger.Errorf("cannot marshal JSON result %#v: %v", response, err)
   159  		return
   160  	}
   161  	body = append(body, '\n')
   162  	w.Write(body)
   163  }
   164  
   165  // sendStatusAndJSON sends an HTTP status code and
   166  // a JSON-encoded response to a client.
   167  func sendStatusAndJSON(w http.ResponseWriter, statusCode int, response interface{}) {
   168  	body, err := json.Marshal(response)
   169  	if err != nil {
   170  		logger.Errorf("cannot marshal JSON result %#v: %v", response, err)
   171  		return
   172  	}
   173  
   174  	if statusCode == http.StatusUnauthorized {
   175  		w.Header().Set("WWW-Authenticate", `Basic realm="juju"`)
   176  	}
   177  	w.Header().Set("Content-Type", params.ContentTypeJSON)
   178  	w.Header().Set("Content-Length", fmt.Sprint(len(body)))
   179  	w.WriteHeader(statusCode)
   180  	w.Write(body)
   181  }
   182  
   183  // sendError sends a JSON-encoded error response
   184  // for errors encountered during processing.
   185  func sendError(w http.ResponseWriter, err error) {
   186  	err1, statusCode := common.ServerErrorAndStatus(err)
   187  	logger.Debugf("sending error: %d %v", statusCode, err1)
   188  	sendStatusAndJSON(w, statusCode, &params.ErrorResult{
   189  		Error: err1,
   190  	})
   191  }