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  }