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 }