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  }