github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/server_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "context" 8 "crypto/x509" 9 "fmt" 10 "net" 11 "net/http" 12 13 "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" 14 "github.com/gorilla/websocket" 15 jujuerrors "github.com/juju/errors" 16 jujuhttp "github.com/juju/http/v2" 17 "github.com/juju/loggo" 18 "github.com/juju/names/v5" 19 jc "github.com/juju/testing/checkers" 20 "github.com/juju/utils/v3" 21 gc "gopkg.in/check.v1" 22 "gopkg.in/macaroon.v2" 23 24 "github.com/juju/juju/api" 25 apimachiner "github.com/juju/juju/api/agent/machiner" 26 "github.com/juju/juju/apiserver" 27 "github.com/juju/juju/apiserver/authentication" 28 "github.com/juju/juju/apiserver/authentication/jwt" 29 "github.com/juju/juju/apiserver/errors" 30 apitesting "github.com/juju/juju/apiserver/testing" 31 "github.com/juju/juju/apiserver/testserver" 32 "github.com/juju/juju/core/network" 33 "github.com/juju/juju/core/permission" 34 jujutesting "github.com/juju/juju/juju/testing" 35 "github.com/juju/juju/rpc/params" 36 "github.com/juju/juju/state" 37 coretesting "github.com/juju/juju/testing" 38 "github.com/juju/juju/testing/factory" 39 ) 40 41 var fastDialOpts = api.DialOpts{} 42 43 type serverSuite struct { 44 jujutesting.JujuConnSuite 45 } 46 47 var _ = gc.Suite(&serverSuite{}) 48 49 func (s *serverSuite) TestStop(c *gc.C) { 50 // Start our own instance of the server so we have 51 // a handle on it to stop it. 52 srv := testserver.NewServer(c, s.StatePool, s.Controller) 53 defer assertStop(c, srv) 54 55 machine, password := s.Factory.MakeMachineReturningPassword( 56 c, &factory.MachineParams{Nonce: "fake_nonce"}) 57 58 // Note we can't use openAs because we're not connecting to 59 info := srv.Info 60 info.Tag = machine.Tag() 61 info.Password = password 62 info.Nonce = "fake_nonce" 63 info.ModelTag = s.Model.ModelTag() 64 65 st, err := api.Open(info, fastDialOpts) 66 c.Assert(err, jc.ErrorIsNil) 67 defer st.Close() 68 69 _, err = apimachiner.NewState(st).Machine(machine.MachineTag()) 70 c.Assert(err, jc.ErrorIsNil) 71 72 err = srv.Stop() 73 c.Assert(err, jc.ErrorIsNil) 74 75 _, err = apimachiner.NewState(st).Machine(machine.MachineTag()) 76 // The client has not necessarily seen the server shutdown yet, so there 77 // are multiple possible errors. All we should care about is that there is 78 // an error, not what the error actually is. 79 c.Assert(err, gc.NotNil) 80 81 // Check it can be stopped twice. 82 err = srv.Stop() 83 c.Assert(err, jc.ErrorIsNil) 84 } 85 86 func (s *serverSuite) TestAPIServerCanListenOnBothIPv4AndIPv6(c *gc.C) { 87 err := s.State.SetAPIHostPorts(nil) 88 c.Assert(err, jc.ErrorIsNil) 89 90 // Start our own instance of the server listening on 91 // both IPv4 and IPv6 localhost addresses and an ephemeral port. 92 srv := testserver.NewServer(c, s.StatePool, s.Controller) 93 defer assertStop(c, srv) 94 95 machine, password := s.Factory.MakeMachineReturningPassword( 96 c, &factory.MachineParams{Nonce: "fake_nonce"}) 97 98 info := srv.Info 99 port := info.Ports()[0] 100 portString := fmt.Sprintf("%d", port) 101 102 // Now connect twice - using IPv4 and IPv6 endpoints. 103 info.Tag = machine.Tag() 104 info.Password = password 105 info.Nonce = "fake_nonce" 106 info.ModelTag = s.Model.ModelTag() 107 108 ipv4State, err := api.Open(info, fastDialOpts) 109 c.Assert(err, jc.ErrorIsNil) 110 defer ipv4State.Close() 111 c.Assert(ipv4State.Addr(), gc.Equals, net.JoinHostPort("localhost", portString)) 112 c.Assert(ipv4State.APIHostPorts(), jc.DeepEquals, []network.MachineHostPorts{ 113 network.NewMachineHostPorts(port, "localhost"), 114 }) 115 116 _, err = apimachiner.NewState(ipv4State).Machine(machine.MachineTag()) 117 c.Assert(err, jc.ErrorIsNil) 118 119 info.Addrs = []string{net.JoinHostPort("::1", portString)} 120 ipv6State, err := api.Open(info, fastDialOpts) 121 c.Assert(err, jc.ErrorIsNil) 122 defer ipv6State.Close() 123 c.Assert(ipv6State.Addr(), gc.Equals, net.JoinHostPort("::1", portString)) 124 c.Assert(ipv6State.APIHostPorts(), jc.DeepEquals, []network.MachineHostPorts{ 125 network.NewMachineHostPorts(port, "::1"), 126 }) 127 128 _, err = apimachiner.NewState(ipv6State).Machine(machine.MachineTag()) 129 c.Assert(err, jc.ErrorIsNil) 130 } 131 132 func (s *serverSuite) TestOpenAsMachineErrors(c *gc.C) { 133 assertNotProvisioned := func(err error) { 134 c.Assert(err, gc.NotNil) 135 c.Assert(err, jc.Satisfies, params.IsCodeNotProvisioned) 136 c.Assert(err, gc.ErrorMatches, `machine \d+ not provisioned \(not provisioned\)`) 137 } 138 139 machine, password := s.Factory.MakeMachineReturningPassword( 140 c, &factory.MachineParams{Nonce: "fake_nonce"}) 141 142 // This does almost exactly the same as OpenAPIAsMachine but checks 143 // for failures instead. 144 info := s.APIInfo(c) 145 info.Tag = machine.Tag() 146 info.Password = password 147 info.Nonce = "invalid-nonce" 148 st, err := api.Open(info, fastDialOpts) 149 assertNotProvisioned(err) 150 c.Assert(st, gc.IsNil) 151 152 // Try with empty nonce as well. 153 info.Nonce = "" 154 st, err = api.Open(info, fastDialOpts) 155 assertNotProvisioned(err) 156 c.Assert(st, gc.IsNil) 157 158 // Finally, with the correct one succeeds. 159 info.Nonce = "fake_nonce" 160 st, err = api.Open(info, fastDialOpts) 161 c.Assert(err, jc.ErrorIsNil) 162 c.Assert(st, gc.NotNil) 163 st.Close() 164 165 // Now add another machine, intentionally unprovisioned. 166 stm1, err := s.State.AddMachine(state.UbuntuBase("12.10"), state.JobHostUnits) 167 c.Assert(err, jc.ErrorIsNil) 168 err = stm1.SetPassword(password) 169 c.Assert(err, jc.ErrorIsNil) 170 171 // Try connecting, it will fail. 172 info.Tag = stm1.Tag() 173 info.Nonce = "" 174 st, err = api.Open(info, fastDialOpts) 175 assertNotProvisioned(err) 176 c.Assert(st, gc.IsNil) 177 } 178 179 func dialWebsocket(c *gc.C, addr, path string) (*websocket.Conn, error) { 180 // TODO(rogpeppe) merge this with the very similar dialWebsocketFromURL function. 181 url := fmt.Sprintf("wss://%s%s", addr, path) 182 header := make(http.Header) 183 header.Set("Origin", "http://localhost/") 184 caCerts := x509.NewCertPool() 185 c.Assert(caCerts.AppendCertsFromPEM([]byte(coretesting.CACert)), jc.IsTrue) 186 tlsConfig := jujuhttp.SecureTLSConfig() 187 tlsConfig.RootCAs = caCerts 188 tlsConfig.ServerName = "anything" 189 190 dialer := &websocket.Dialer{ 191 TLSClientConfig: tlsConfig, 192 } 193 conn, _, err := dialer.Dial(url, header) 194 return conn, err 195 } 196 197 func (s *serverSuite) TestNonCompatiblePathsAre404(c *gc.C) { 198 // We expose the API at '/api', '/' (controller-only), and at '/ModelUUID/api' 199 // for the correct location, but other paths should fail. 200 loggo.GetLogger("juju.apiserver").SetLogLevel(loggo.TRACE) 201 srv := testserver.NewServer(c, s.StatePool, s.Controller) 202 defer assertStop(c, srv) 203 204 // We have to use 'localhost' because that is what the TLS cert says. 205 addr := fmt.Sprintf("localhost:%d", srv.Info.Ports()[0]) 206 207 // '/api' should be fine 208 conn, err := dialWebsocket(c, addr, "/api") 209 c.Assert(err, jc.ErrorIsNil) 210 conn.Close() 211 212 // '/`' should be fine 213 conn, err = dialWebsocket(c, addr, "/") 214 c.Assert(err, jc.ErrorIsNil) 215 conn.Close() 216 217 // '/model/MODELUUID/api' should be fine 218 conn, err = dialWebsocket(c, addr, "/model/deadbeef-1234-5678-0123-0123456789ab/api") 219 c.Assert(err, jc.ErrorIsNil) 220 conn.Close() 221 222 // '/randompath' is not ok 223 conn, err = dialWebsocket(c, addr, "/randompath") 224 // Unfortunately gorilla/websocket just returns bad handshake, it doesn't 225 // give us any information (whether this was a 404 Not Found, Internal 226 // Server Error, 200 OK, etc.) 227 c.Assert(err, gc.ErrorMatches, `websocket: bad handshake`) 228 c.Assert(conn, gc.IsNil) 229 } 230 231 type fakeResource struct { 232 stopped bool 233 } 234 235 func (r *fakeResource) Stop() error { 236 r.stopped = true 237 return nil 238 } 239 240 func (s *serverSuite) bootstrapHasPermissionTest(c *gc.C) (*state.User, names.ControllerTag) { 241 u, err := s.State.AddUser("foobar", "Foo Bar", "password", "read") 242 c.Assert(err, jc.ErrorIsNil) 243 user := u.UserTag() 244 245 ctag, err := names.ParseControllerTag("controller-" + s.State.ControllerUUID()) 246 c.Assert(err, jc.ErrorIsNil) 247 access, err := s.State.UserPermission(user, ctag) 248 c.Assert(err, jc.ErrorIsNil) 249 c.Assert(access, gc.Equals, permission.LoginAccess) 250 return u, ctag 251 } 252 253 func (s *serverSuite) TestAPIHandlerHasPermissionLogin(c *gc.C) { 254 u, ctag := s.bootstrapHasPermissionTest(c) 255 256 handler, _ := apiserver.TestingAPIHandlerWithEntity(c, s.StatePool, s.State, u) 257 defer handler.Kill() 258 259 apiserver.AssertHasPermission(c, handler, permission.LoginAccess, ctag, true) 260 apiserver.AssertHasPermission(c, handler, permission.SuperuserAccess, ctag, false) 261 } 262 263 func (s *serverSuite) TestAPIHandlerHasPermissionSuperUser(c *gc.C) { 264 u, ctag := s.bootstrapHasPermissionTest(c) 265 user := u.UserTag() 266 267 handler, _ := apiserver.TestingAPIHandlerWithEntity(c, s.StatePool, s.State, u) 268 defer handler.Kill() 269 270 ua, err := s.State.SetUserAccess(user, ctag, permission.SuperuserAccess) 271 c.Assert(err, jc.ErrorIsNil) 272 c.Assert(ua.Access, gc.Equals, permission.SuperuserAccess) 273 274 apiserver.AssertHasPermission(c, handler, permission.LoginAccess, ctag, true) 275 apiserver.AssertHasPermission(c, handler, permission.SuperuserAccess, ctag, true) 276 } 277 278 func (s *serverSuite) TestAPIHandlerHasPermissionLoginToken(c *gc.C) { 279 user := names.NewUserTag("fred") 280 token, err := apitesting.NewJWT(apitesting.JWTParams{ 281 Controller: coretesting.ControllerTag.Id(), 282 User: user.String(), 283 Access: map[string]string{ 284 coretesting.ControllerTag.String(): "superuser", 285 coretesting.ModelTag.String(): "write", 286 }, 287 }) 288 c.Assert(err, jc.ErrorIsNil) 289 290 delegator := &jwt.PermissionDelegator{token} 291 handler, _ := apiserver.TestingAPIHandlerWithToken(c, s.StatePool, s.State, token, delegator) 292 defer handler.Kill() 293 294 apiserver.AssertHasPermission(c, handler, permission.LoginAccess, coretesting.ControllerTag, true) 295 apiserver.AssertHasPermission(c, handler, permission.SuperuserAccess, coretesting.ControllerTag, true) 296 apiserver.AssertHasPermission(c, handler, permission.WriteAccess, coretesting.ModelTag, true) 297 } 298 299 func (s *serverSuite) TestAPIHandlerMissingPermissionLoginToken(c *gc.C) { 300 user := names.NewUserTag("fred") 301 token, err := apitesting.NewJWT(apitesting.JWTParams{ 302 Controller: coretesting.ControllerTag.Id(), 303 User: user.String(), 304 Access: map[string]string{ 305 coretesting.ControllerTag.String(): "superuser", 306 coretesting.ModelTag.String(): "write", 307 }, 308 }) 309 c.Assert(err, jc.ErrorIsNil) 310 311 delegator := &jwt.PermissionDelegator{token} 312 handler, _ := apiserver.TestingAPIHandlerWithToken(c, s.StatePool, s.State, token, delegator) 313 defer handler.Kill() 314 err = handler.HasPermission(permission.AdminAccess, coretesting.ModelTag) 315 var reqError *errors.AccessRequiredError 316 c.Assert(jujuerrors.As(err, &reqError), jc.IsTrue) 317 c.Assert(reqError, jc.DeepEquals, &errors.AccessRequiredError{ 318 RequiredAccess: map[names.Tag]permission.Access{ 319 coretesting.ModelTag: permission.AdminAccess, 320 }, 321 }) 322 } 323 324 func (s *serverSuite) TestAPIHandlerTeardownInitialModel(c *gc.C) { 325 s.checkAPIHandlerTeardown(c, s.State, s.State) 326 } 327 328 func (s *serverSuite) TestAPIHandlerTeardownOtherModel(c *gc.C) { 329 otherState := s.Factory.MakeModel(c, nil) 330 defer otherState.Close() 331 s.checkAPIHandlerTeardown(c, s.State, otherState) 332 } 333 334 func (s *serverSuite) TestAPIHandlerConnectedModel(c *gc.C) { 335 otherState := s.Factory.MakeModel(c, nil) 336 defer otherState.Close() 337 handler, _ := apiserver.TestingAPIHandler(c, s.StatePool, otherState) 338 defer handler.Kill() 339 c.Check(handler.ConnectedModel(), gc.Equals, otherState.ModelUUID()) 340 } 341 342 func (s *serverSuite) TestClosesStateFromPool(c *gc.C) { 343 cfg := testserver.DefaultServerConfig(c, nil) 344 cfg.Controller = s.Controller 345 server := testserver.NewServerWithConfig(c, s.StatePool, cfg) 346 defer assertStop(c, server) 347 348 otherState := s.Factory.MakeModel(c, nil) 349 defer otherState.Close() 350 351 // Ensure the model's in the pool but not referenced. 352 st, err := s.StatePool.Get(otherState.ModelUUID()) 353 c.Assert(err, jc.ErrorIsNil) 354 st.Release() 355 356 // Make a request for the model API to check it releases 357 // state back into the pool once the connection is closed. 358 addr := fmt.Sprintf("localhost:%d", server.Info.Ports()[0]) 359 conn, err := dialWebsocket(c, addr, fmt.Sprintf("/model/%s/api", st.ModelUUID())) 360 c.Assert(err, jc.ErrorIsNil) 361 conn.Close() 362 363 // Don't make an assertion about whether the remove call returns 364 // true - that's dependent on whether the server has reacted to 365 // the connection being closed yet, so it's racy. 366 _, err = s.StatePool.Remove(otherState.ModelUUID()) 367 c.Assert(err, jc.ErrorIsNil) 368 assertStateBecomesClosed(c, st.State) 369 } 370 371 func assertStateBecomesClosed(c *gc.C, st *state.State) { 372 // This is gross but I can't see any other way to check for 373 // closedness outside the state package. 374 checkModel := func() { 375 attempt := utils.AttemptStrategy{ 376 Total: coretesting.LongWait, 377 Delay: coretesting.ShortWait, 378 } 379 for a := attempt.Start(); a.Next(); { 380 // This will panic once the state is closed. 381 _, _ = st.Model() 382 } 383 // If we got here then st is still open. 384 st.Close() 385 } 386 c.Assert(checkModel, gc.PanicMatches, "Session already closed") 387 } 388 389 func (s *serverSuite) checkAPIHandlerTeardown(c *gc.C, srvSt, st *state.State) { 390 handler, resources := apiserver.TestingAPIHandler(c, s.StatePool, st) 391 resource := new(fakeResource) 392 resources.Register(resource) 393 394 c.Assert(resource.stopped, jc.IsFalse) 395 handler.Kill() 396 c.Assert(resource.stopped, jc.IsTrue) 397 } 398 399 type stopper interface { 400 Stop() error 401 } 402 403 func assertStop(c *gc.C, stopper stopper) { 404 c.Assert(stopper.Stop(), gc.IsNil) 405 } 406 407 type mockAuthenticator struct { 408 } 409 410 func (a *mockAuthenticator) Authenticate(req *http.Request) (authentication.AuthInfo, error) { 411 return authentication.AuthInfo{}, nil 412 } 413 414 func (a *mockAuthenticator) AuthenticateLoginRequest( 415 _ context.Context, 416 _, 417 _ string, 418 authParams authentication.AuthParams, 419 ) (authentication.AuthInfo, error) { 420 return authentication.AuthInfo{ 421 Entity: &mockEntity{tag: authParams.AuthTag}, 422 }, nil 423 } 424 425 func (a *mockAuthenticator) CreateLocalLoginMacaroon(ctx context.Context, tag names.UserTag, version bakery.Version) (*macaroon.Macaroon, error) { 426 return nil, nil 427 } 428 429 type mockEntity struct { 430 tag names.Tag 431 } 432 433 func (e *mockEntity) Tag() names.Tag { 434 return e.tag 435 }