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, ¶ms.ErrorResult{ 189 Error: err1, 190 }) 191 }