github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/apiserver/authhttp_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "bufio" 8 "crypto/x509" 9 "encoding/json" 10 "io" 11 "io/ioutil" 12 "net/http" 13 "net/url" 14 "os" 15 16 "github.com/juju/errors" 17 "github.com/juju/names" 18 jc "github.com/juju/testing/checkers" 19 "github.com/juju/testing/httptesting" 20 "github.com/juju/utils" 21 "golang.org/x/net/websocket" 22 gc "gopkg.in/check.v1" 23 "gopkg.in/macaroon-bakery.v1/httpbakery" 24 25 apitesting "github.com/juju/juju/api/testing" 26 "github.com/juju/juju/apiserver/params" 27 "github.com/juju/juju/state" 28 "github.com/juju/juju/testing" 29 "github.com/juju/juju/testing/factory" 30 ) 31 32 // authHttpSuite provides helpers for testing HTTP "streaming" style APIs. 33 type authHttpSuite struct { 34 // macaroonAuthEnabled may be set by a test suite 35 // before SetUpTest is called. If it is true, macaroon 36 // authentication will be enabled for the duration 37 // of the suite. 38 macaroonAuthEnabled bool 39 40 // MacaroonSuite is embedded because we need 41 // it when macaroonAuthEnabled is true. 42 // When macaroonAuthEnabled is false, 43 // only the JujuConnSuite in it will be initialized; 44 // all other fields will be zero. 45 apitesting.MacaroonSuite 46 47 modelUUID string 48 49 // userTag and password hold the user tag and password 50 // to use in authRequest. When macaroonAuthEnabled 51 // is true, password will be empty. 52 userTag names.UserTag 53 password string 54 } 55 56 func (s *authHttpSuite) SetUpTest(c *gc.C) { 57 if s.macaroonAuthEnabled { 58 s.MacaroonSuite.SetUpTest(c) 59 } else { 60 // No macaroons, so don't enable them. 61 s.JujuConnSuite.SetUpTest(c) 62 } 63 64 s.modelUUID = s.State.ModelUUID() 65 66 if s.macaroonAuthEnabled { 67 // When macaroon authentication is enabled, we must use 68 // an external user. 69 s.userTag = names.NewUserTag("bob@authhttpsuite") 70 s.AddModelUser(c, s.userTag.Id()) 71 } else { 72 // Make a user in the state. 73 s.password = "password" 74 user := s.Factory.MakeUser(c, &factory.UserParams{Password: s.password}) 75 s.userTag = user.UserTag() 76 } 77 } 78 79 func (s *authHttpSuite) TearDownTest(c *gc.C) { 80 if s.macaroonAuthEnabled { 81 s.MacaroonSuite.TearDownTest(c) 82 } else { 83 s.JujuConnSuite.TearDownTest(c) 84 } 85 } 86 87 func (s *authHttpSuite) baseURL(c *gc.C) *url.URL { 88 info := s.APIInfo(c) 89 return &url.URL{ 90 Scheme: "https", 91 Host: info.Addrs[0], 92 Path: "", 93 } 94 } 95 96 func (s *authHttpSuite) dialWebsocketFromURL(c *gc.C, server string, header http.Header) *websocket.Conn { 97 config := s.makeWebsocketConfigFromURL(c, server, header) 98 c.Logf("dialing %v", server) 99 conn, err := websocket.DialConfig(config) 100 c.Assert(err, jc.ErrorIsNil) 101 return conn 102 } 103 104 func (s *authHttpSuite) makeWebsocketConfigFromURL(c *gc.C, server string, header http.Header) *websocket.Config { 105 config, err := websocket.NewConfig(server, "http://localhost/") 106 c.Assert(err, jc.ErrorIsNil) 107 config.Header = header 108 caCerts := x509.NewCertPool() 109 c.Assert(caCerts.AppendCertsFromPEM([]byte(testing.CACert)), jc.IsTrue) 110 config.TlsConfig = utils.SecureTLSConfig() 111 config.TlsConfig.RootCAs = caCerts 112 config.TlsConfig.ServerName = "anything" 113 return config 114 } 115 116 func (s *authHttpSuite) assertWebsocketClosed(c *gc.C, reader *bufio.Reader) { 117 _, err := reader.ReadByte() 118 c.Assert(err, gc.Equals, io.EOF) 119 } 120 121 func (s *authHttpSuite) makeURL(c *gc.C, scheme, path string, queryParams url.Values) *url.URL { 122 url := s.baseURL(c) 123 query := "" 124 if queryParams != nil { 125 query = queryParams.Encode() 126 } 127 url.Scheme = scheme 128 url.Path += path 129 url.RawQuery = query 130 return url 131 } 132 133 // httpRequestParams holds parameters for the authRequest and sendRequest 134 // methods. 135 type httpRequestParams struct { 136 // do is used to make the HTTP request. 137 // If it is nil, utils.GetNonValidatingHTTPClient().Do will be used. 138 // If the body reader implements io.Seeker, 139 // req.Body will also implement that interface. 140 do func(req *http.Request) (*http.Response, error) 141 142 // expectError holds the error regexp to match 143 // against the error returned from the HTTP Do 144 // request. If it is empty, the error is expected to be 145 // nil. 146 expectError string 147 148 // tag holds the tag to authenticate as. 149 tag string 150 151 // password holds the password associated with the tag. 152 password string 153 154 // method holds the HTTP method to use for the request. 155 method string 156 157 // url holds the URL to send the HTTP request to. 158 url string 159 160 // contentType holds the content type of the request. 161 contentType string 162 163 // body holds the body of the request. 164 body io.Reader 165 166 // jsonBody holds an object to be marshaled as JSON 167 // as the body of the request. If this is specified, body will 168 // be ignored and the Content-Type header will 169 // be set to application/json. 170 jsonBody interface{} 171 172 // nonce holds the machine nonce to provide in the header. 173 nonce string 174 } 175 176 func (s *authHttpSuite) sendRequest(c *gc.C, p httpRequestParams) *http.Response { 177 c.Logf("sendRequest: %s", p.url) 178 hp := httptesting.DoRequestParams{ 179 Do: p.do, 180 Method: p.method, 181 URL: p.url, 182 Body: p.body, 183 JSONBody: p.jsonBody, 184 Header: make(http.Header), 185 Username: p.tag, 186 Password: p.password, 187 ExpectError: p.expectError, 188 } 189 if p.contentType != "" { 190 hp.Header.Set("Content-Type", p.contentType) 191 } 192 if p.nonce != "" { 193 hp.Header.Set(params.MachineNonceHeader, p.nonce) 194 } 195 if hp.Do == nil { 196 hp.Do = utils.GetNonValidatingHTTPClient().Do 197 } 198 return httptesting.Do(c, hp) 199 } 200 201 // bakeryDo provides a function suitable for using in httpRequestParams.Do 202 // that will use the given http client (or utils.GetNonValidatingHTTPClient() 203 // if client is nil) and use the given getBakeryError function 204 // to translate errors in responses. 205 func bakeryDo(client *http.Client, getBakeryError func(*http.Response) error) func(*http.Request) (*http.Response, error) { 206 bclient := httpbakery.NewClient() 207 if client != nil { 208 bclient.Client = client 209 } else { 210 // Configure the default client to skip verification/ 211 tlsConfig := utils.SecureTLSConfig() 212 tlsConfig.InsecureSkipVerify = true 213 bclient.Client.Transport = utils.NewHttpTLSTransport(tlsConfig) 214 } 215 return func(req *http.Request) (*http.Response, error) { 216 var body io.ReadSeeker 217 if req.Body != nil { 218 body = req.Body.(io.ReadSeeker) 219 req.Body = nil 220 } 221 return bclient.DoWithBodyAndCustomError(req, body, getBakeryError) 222 } 223 } 224 225 // authRequest is like sendRequest but fills out p.tag and p.password 226 // from the userTag and password fields in the suite. 227 func (s *authHttpSuite) authRequest(c *gc.C, p httpRequestParams) *http.Response { 228 p.tag = s.userTag.String() 229 p.password = s.password 230 return s.sendRequest(c, p) 231 } 232 233 func (s *authHttpSuite) setupOtherModel(c *gc.C) *state.State { 234 envState := s.Factory.MakeModel(c, nil) 235 s.AddCleanup(func(*gc.C) { envState.Close() }) 236 user := s.Factory.MakeUser(c, nil) 237 _, err := envState.AddModelUser(state.ModelUserSpec{ 238 User: user.UserTag(), 239 CreatedBy: s.userTag}) 240 c.Assert(err, jc.ErrorIsNil) 241 s.userTag = user.UserTag() 242 s.password = "password" 243 s.modelUUID = envState.ModelUUID() 244 return envState 245 } 246 247 func (s *authHttpSuite) uploadRequest(c *gc.C, uri string, contentType, path string) *http.Response { 248 if path == "" { 249 return s.authRequest(c, httpRequestParams{ 250 method: "POST", 251 url: uri, 252 contentType: contentType, 253 }) 254 } 255 256 file, err := os.Open(path) 257 c.Assert(err, jc.ErrorIsNil) 258 defer file.Close() 259 return s.authRequest(c, httpRequestParams{ 260 method: "POST", 261 url: uri, 262 contentType: contentType, 263 body: file, 264 }) 265 } 266 267 // assertJSONError checks the JSON encoded error returned by the log 268 // and logsink APIs matches the expected value. 269 func assertJSONError(c *gc.C, reader *bufio.Reader, expected string) { 270 errResult := readJSONErrorLine(c, reader) 271 c.Assert(errResult.Error, gc.NotNil) 272 c.Assert(errResult.Error.Message, gc.Matches, expected) 273 } 274 275 // readJSONErrorLine returns the error line returned by the log and 276 // logsink APIS. 277 func readJSONErrorLine(c *gc.C, reader *bufio.Reader) params.ErrorResult { 278 line, err := reader.ReadSlice('\n') 279 c.Assert(err, jc.ErrorIsNil) 280 var errResult params.ErrorResult 281 err = json.Unmarshal(line, &errResult) 282 c.Assert(err, jc.ErrorIsNil) 283 return errResult 284 } 285 286 func assertResponse(c *gc.C, resp *http.Response, expHTTPStatus int, expContentType string) []byte { 287 body, err := ioutil.ReadAll(resp.Body) 288 resp.Body.Close() 289 c.Assert(err, jc.ErrorIsNil) 290 c.Check(resp.StatusCode, gc.Equals, expHTTPStatus, gc.Commentf("body: %s", body)) 291 ctype := resp.Header.Get("Content-Type") 292 c.Assert(ctype, gc.Equals, expContentType) 293 return body 294 } 295 296 // bakeryGetError implements a getError function 297 // appropriate for passing to httpbakery.Client.DoWithBodyAndCustomError 298 // for any endpoint that returns the error in a top level Error field. 299 func bakeryGetError(resp *http.Response) error { 300 if resp.StatusCode != http.StatusUnauthorized { 301 return nil 302 } 303 data, err := ioutil.ReadAll(resp.Body) 304 if err != nil { 305 return errors.Annotatef(err, "cannot read body") 306 } 307 var errResp params.ErrorResult 308 if err := json.Unmarshal(data, &errResp); err != nil { 309 return errors.Annotatef(err, "cannot unmarshal body") 310 } 311 if errResp.Error == nil { 312 return errors.New("no error found in error response body") 313 } 314 if errResp.Error.Code != params.CodeDischargeRequired { 315 return errResp.Error 316 } 317 if errResp.Error.Info == nil { 318 return errors.Annotatef(err, "no error info found in discharge-required response error") 319 } 320 // It's a discharge-required error, so make an appropriate httpbakery 321 // error from it. 322 return &httpbakery.Error{ 323 Message: errResp.Error.Message, 324 Code: httpbakery.ErrDischargeRequired, 325 Info: &httpbakery.ErrorInfo{ 326 Macaroon: errResp.Error.Info.Macaroon, 327 MacaroonPath: errResp.Error.Info.MacaroonPath, 328 }, 329 } 330 }