github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/api/state_macaroon_test.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api_test
     5  
     6  import (
     7  	"net/http"
     8  	"net/url"
     9  
    10  	"github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery"
    11  	jc "github.com/juju/testing/checkers"
    12  	gc "gopkg.in/check.v1"
    13  	"gopkg.in/macaroon.v2"
    14  
    15  	"github.com/juju/juju/api"
    16  	apitesting "github.com/juju/juju/api/testing"
    17  	"github.com/juju/juju/core/permission"
    18  )
    19  
    20  var _ = gc.Suite(&macaroonLoginSuite{})
    21  
    22  type macaroonLoginSuite struct {
    23  	apitesting.MacaroonSuite
    24  	client   api.Connection
    25  	macSlice []macaroon.Slice
    26  }
    27  
    28  const testUserName = "testuser@somewhere"
    29  
    30  func (s *macaroonLoginSuite) SetUpTest(c *gc.C) {
    31  	s.MacaroonSuite.SetUpTest(c)
    32  	mac, err := apitesting.NewMacaroon("test")
    33  	c.Assert(err, jc.ErrorIsNil)
    34  	s.macSlice = []macaroon.Slice{{mac}}
    35  	s.AddModelUser(c, testUserName)
    36  	s.AddControllerUser(c, testUserName, permission.LoginAccess)
    37  	info := s.APIInfo(c)
    38  	info.Macaroons = nil
    39  	info.SkipLogin = true
    40  	s.client = s.OpenAPI(c, info, nil)
    41  }
    42  
    43  func (s *macaroonLoginSuite) TearDownTest(c *gc.C) {
    44  	s.client.Close()
    45  	s.MacaroonSuite.TearDownTest(c)
    46  }
    47  
    48  func (s *macaroonLoginSuite) TestSuccessfulLogin(c *gc.C) {
    49  	s.DischargerLogin = func() string { return testUserName }
    50  	err := s.client.Login(nil, "", "", s.macSlice)
    51  	c.Assert(err, jc.ErrorIsNil)
    52  }
    53  
    54  func (s *macaroonLoginSuite) TestFailedToObtainDischargeLogin(c *gc.C) {
    55  	err := s.client.Login(nil, "", "", s.macSlice)
    56  	c.Assert(err, gc.ErrorMatches, `cannot get discharge from "https://.*": third party refused discharge: cannot discharge: login denied by discharger`)
    57  }
    58  
    59  func (s *macaroonLoginSuite) TestConnectStream(c *gc.C) {
    60  	catcher := api.UrlCatcher{}
    61  	s.PatchValue(&api.WebsocketDial, catcher.RecordLocation)
    62  
    63  	dischargeCount := 0
    64  	s.DischargerLogin = func() string {
    65  		dischargeCount++
    66  		return testUserName
    67  	}
    68  	// First log into the regular API.
    69  	err := s.client.Login(nil, "", "", s.macSlice)
    70  	c.Assert(err, jc.ErrorIsNil)
    71  	c.Assert(dischargeCount, gc.Equals, 1)
    72  
    73  	// Then check that ConnectStream works OK and that it doesn't need
    74  	// to discharge again.
    75  	conn, err := s.client.ConnectStream("/path", nil)
    76  	c.Assert(err, gc.IsNil)
    77  	defer conn.Close()
    78  	connectURL, err := url.Parse(catcher.Location())
    79  	c.Assert(err, jc.ErrorIsNil)
    80  	c.Assert(connectURL.Path, gc.Equals, "/model/"+s.Model.ModelTag().Id()+"/path")
    81  	c.Assert(dischargeCount, gc.Equals, 1)
    82  }
    83  
    84  func (s *macaroonLoginSuite) TestConnectStreamWithoutLogin(c *gc.C) {
    85  	catcher := api.UrlCatcher{}
    86  	s.PatchValue(&api.WebsocketDial, catcher.RecordLocation)
    87  
    88  	conn, err := s.client.ConnectStream("/path", nil)
    89  	c.Assert(err, gc.ErrorMatches, `cannot use ConnectStream without logging in`)
    90  	c.Assert(conn, gc.Equals, nil)
    91  }
    92  
    93  func (s *macaroonLoginSuite) TestConnectStreamFailedDischarge(c *gc.C) {
    94  	// This is really a test for ConnectStream, but to test ConnectStream's
    95  	// discharge failing logic, we need an actual endpoint to test against,
    96  	// and the debug-log endpoint makes a convenient example.
    97  
    98  	var dischargeError bool
    99  	s.DischargerLogin = func() string {
   100  		if dischargeError {
   101  			return ""
   102  		}
   103  		return testUserName
   104  	}
   105  
   106  	// Make an API connection that uses a cookie jar
   107  	// that allows us to remove all cookies.
   108  	jar := apitesting.NewClearableCookieJar()
   109  	client := s.OpenAPI(c, nil, jar)
   110  
   111  	// Ensure that the discharger won't discharge and try
   112  	// logging in again. We should succeed in getting past
   113  	// authorization because we have the cookies (but
   114  	// the actual debug-log endpoint will return an error).
   115  	dischargeError = true
   116  	logArgs := url.Values{"noTail": []string{"true"}}
   117  	conn, err := client.ConnectStream("/log", logArgs)
   118  	c.Assert(err, jc.ErrorIsNil)
   119  	c.Assert(conn, gc.NotNil)
   120  	conn.Close()
   121  
   122  	// Then delete all the cookies by deleting the cookie jar
   123  	// and try again. The login should fail.
   124  	jar.Clear()
   125  
   126  	conn, err = client.ConnectStream("/log", logArgs)
   127  	c.Assert(err, gc.ErrorMatches, `cannot get discharge from "https://.*": third party refused discharge: cannot discharge: login denied by discharger`)
   128  	c.Assert(conn, gc.IsNil)
   129  }
   130  
   131  func (s *macaroonLoginSuite) TestConnectStreamWithDischargedMacaroons(c *gc.C) {
   132  	// If the connection was created with already-discharged macaroons
   133  	// (rather than acquiring them through the discharge dance), they
   134  	// wouldn't get attached to the websocket request.
   135  	// https://bugs.launchpad.net/juju/+bug/1650451
   136  	catcher := api.UrlCatcher{}
   137  	s.PatchValue(&api.WebsocketDial, catcher.RecordLocation)
   138  
   139  	mac, err := macaroon.New([]byte("abc-123"), []byte("aurora gone"), "shankil butchers", macaroon.LatestVersion)
   140  	c.Assert(err, jc.ErrorIsNil)
   141  
   142  	s.DischargerLogin = func() string {
   143  		return testUserName
   144  	}
   145  
   146  	info := s.APIInfo(c)
   147  	info.Macaroons = []macaroon.Slice{{mac}}
   148  	client := s.OpenAPI(c, info, nil)
   149  
   150  	dischargedMacaroons, err := api.ExtractMacaroons(client)
   151  	c.Assert(err, gc.IsNil)
   152  	c.Assert(len(dischargedMacaroons), gc.Equals, 1)
   153  
   154  	// Mirror the situation in migration logtransfer - the macaroon is
   155  	// now stored in the auth service (so no further discharge is
   156  	// needed), but we use a different client to connect to the log
   157  	// stream, so the macaroon isn't in the cookie jar despite being
   158  	// in the connection info.
   159  
   160  	// Then check that ConnectStream works OK and that it doesn't need
   161  	// to discharge again.
   162  	s.DischargerLogin = nil
   163  
   164  	info2 := s.APIInfo(c)
   165  	info2.Macaroons = dischargedMacaroons
   166  
   167  	client2 := s.OpenAPI(c, info2, nil)
   168  	conn, err := client2.ConnectStream("/path", nil)
   169  	c.Assert(err, gc.IsNil)
   170  	defer conn.Close()
   171  
   172  	headers := catcher.Headers()
   173  	c.Assert(headers.Get(httpbakery.BakeryProtocolHeader), gc.Equals, "3")
   174  	c.Assert(headers.Get("Cookie"), jc.HasPrefix, "macaroon-")
   175  	assertHeaderMatchesMacaroon(c, headers, dischargedMacaroons[0])
   176  }
   177  
   178  func assertHeaderMatchesMacaroon(c *gc.C, header http.Header, macaroon macaroon.Slice) {
   179  	req := http.Request{Header: header}
   180  	actualCookie := req.Cookies()[0]
   181  	expectedCookie, err := httpbakery.NewCookie(nil, macaroon)
   182  	c.Assert(err, jc.ErrorIsNil)
   183  	c.Assert(actualCookie.Name, gc.Equals, expectedCookie.Name)
   184  	c.Assert(actualCookie.Value, gc.Equals, expectedCookie.Value)
   185  }