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