github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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  	"gopkg.in/juju/names.v2"
    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  	authenticator := ctxt.srv.authCtxt.authenticator(r.Host)
    66  	entity, _, err := checkCreds(st, req, true, authenticator)
    67  	if err != nil {
    68  		if common.IsDischargeRequiredError(err) {
    69  			return nil, nil, errors.Trace(err)
    70  		}
    71  
    72  		// Handle the special case of a worker on a controller machine
    73  		// acting on behalf of a hosted model.
    74  		if isMachineTag(req.AuthTag) {
    75  			entity, err := checkControllerMachineCreds(ctxt.srv.state, req, authenticator)
    76  			if err != nil {
    77  				return nil, nil, errors.NewUnauthorized(err, "")
    78  			}
    79  			return st, entity, nil
    80  		}
    81  
    82  		// Any other error at this point should be treated as
    83  		// "unauthorized".
    84  		return nil, nil, errors.Trace(errors.NewUnauthorized(err, ""))
    85  	}
    86  	return st, entity, nil
    87  }
    88  
    89  func isMachineTag(tag string) bool {
    90  	kind, err := names.TagKind(tag)
    91  	return err == nil && kind == names.MachineTagKind
    92  }
    93  
    94  // checkPermissions verifies that given tag passes authentication check.
    95  // For example, if only user tags are accepted, all other tags will be denied access.
    96  func checkPermissions(tag names.Tag, acceptFunc common.GetAuthFunc) (bool, error) {
    97  	accept, err := acceptFunc()
    98  	if err != nil {
    99  		return false, errors.Trace(err)
   100  	}
   101  	if accept(tag) {
   102  		return true, nil
   103  	}
   104  	return false, errors.NotValidf("tag kind %v", tag.Kind())
   105  }
   106  
   107  // stateForRequestAuthenticatedUser is like stateForRequestAuthenticated
   108  // except that it also verifies that the authenticated entity is a user.
   109  func (ctxt *httpContext) stateForRequestAuthenticatedUser(r *http.Request) (*state.State, state.Entity, error) {
   110  	st, entity, err := ctxt.stateForRequestAuthenticated(r)
   111  	if err != nil {
   112  		return nil, nil, errors.Trace(err)
   113  	}
   114  	if ok, err := checkPermissions(entity.Tag(), common.AuthFuncForTagKind(names.UserTagKind)); !ok {
   115  		return nil, nil, err
   116  	}
   117  	return st, entity, nil
   118  }
   119  
   120  // stateForRequestAuthenticatedAgent is like stateForRequestAuthenticated
   121  // except that it also verifies that the authenticated entity is an agent.
   122  func (ctxt *httpContext) stateForRequestAuthenticatedAgent(r *http.Request) (*state.State, state.Entity, error) {
   123  	authFunc := common.AuthEither(
   124  		common.AuthFuncForTagKind(names.MachineTagKind),
   125  		common.AuthFuncForTagKind(names.UnitTagKind),
   126  	)
   127  	st, entity, err := ctxt.stateForRequestAuthenticated(r)
   128  	if err != nil {
   129  		return nil, nil, errors.Trace(err)
   130  	}
   131  	if ok, err := checkPermissions(entity.Tag(), authFunc); !ok {
   132  		return nil, nil, err
   133  	}
   134  	return st, entity, nil
   135  }
   136  
   137  // loginRequest forms a LoginRequest from the information
   138  // in the given HTTP request.
   139  func (ctxt *httpContext) loginRequest(r *http.Request) (params.LoginRequest, error) {
   140  	authHeader := r.Header.Get("Authorization")
   141  	if authHeader == "" {
   142  		// No authorization header implies an attempt
   143  		// to login with external user macaroon authentication.
   144  		return params.LoginRequest{
   145  			Macaroons: httpbakery.RequestMacaroons(r),
   146  		}, nil
   147  	}
   148  	parts := strings.Fields(authHeader)
   149  	if len(parts) != 2 || parts[0] != "Basic" {
   150  		// Invalid header format or no header provided.
   151  		return params.LoginRequest{}, errors.NotValidf("request format")
   152  	}
   153  	// Challenge is a base64-encoded "tag:pass" string.
   154  	// See RFC 2617, Section 2.
   155  	challenge, err := base64.StdEncoding.DecodeString(parts[1])
   156  	if err != nil {
   157  		return params.LoginRequest{}, errors.NotValidf("request format")
   158  	}
   159  	tagPass := strings.SplitN(string(challenge), ":", 2)
   160  	if len(tagPass) != 2 {
   161  		return params.LoginRequest{}, errors.NotValidf("request format")
   162  	}
   163  	// Ensure that a sensible tag was passed.
   164  	_, err = names.ParseTag(tagPass[0])
   165  	if err != nil {
   166  		return params.LoginRequest{}, errors.Trace(err)
   167  	}
   168  	return params.LoginRequest{
   169  		AuthTag:     tagPass[0],
   170  		Credentials: tagPass[1],
   171  		Macaroons:   httpbakery.RequestMacaroons(r),
   172  		Nonce:       r.Header.Get(params.MachineNonceHeader),
   173  	}, nil
   174  }
   175  
   176  // stop returns a channel which will be closed when a handler should
   177  // exit.
   178  func (ctxt *httpContext) stop() <-chan struct{} {
   179  	return ctxt.srv.tomb.Dying()
   180  }
   181  
   182  // sendJSON writes a JSON-encoded response value
   183  // to the given writer along with a trailing newline.
   184  func sendJSON(w io.Writer, response interface{}) error {
   185  	body, err := json.Marshal(response)
   186  	if err != nil {
   187  		logger.Errorf("cannot marshal JSON result %#v: %v", response, err)
   188  		return err
   189  	}
   190  	body = append(body, '\n')
   191  	_, err = w.Write(body)
   192  	return err
   193  }
   194  
   195  // sendStatusAndJSON sends an HTTP status code and
   196  // a JSON-encoded response to a client.
   197  func sendStatusAndJSON(w http.ResponseWriter, statusCode int, response interface{}) error {
   198  	body, err := json.Marshal(response)
   199  	if err != nil {
   200  		return errors.Errorf("cannot marshal JSON result %#v: %v", response, err)
   201  	}
   202  
   203  	if statusCode == http.StatusUnauthorized {
   204  		w.Header().Set("WWW-Authenticate", `Basic realm="juju"`)
   205  	}
   206  	w.Header().Set("Content-Type", params.ContentTypeJSON)
   207  	w.Header().Set("Content-Length", fmt.Sprint(len(body)))
   208  	w.WriteHeader(statusCode)
   209  	if _, err := w.Write(body); err != nil {
   210  		return errors.Annotate(err, "cannot write response")
   211  	}
   212  	return nil
   213  }
   214  
   215  // sendError sends a JSON-encoded error response
   216  // for errors encountered during processing.
   217  func sendError(w http.ResponseWriter, errToSend error) error {
   218  	paramsErr, statusCode := common.ServerErrorAndStatus(errToSend)
   219  	logger.Debugf("sending error: %d %v", statusCode, paramsErr)
   220  	return errors.Trace(sendStatusAndJSON(w, statusCode, &params.ErrorResult{
   221  		Error: paramsErr,
   222  	}))
   223  }