github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/pubsub_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "time" 12 13 "github.com/gorilla/websocket" 14 jujuhttp "github.com/juju/http/v2" 15 "github.com/juju/loggo" 16 "github.com/juju/names/v5" 17 "github.com/juju/pubsub/v2" 18 jc "github.com/juju/testing/checkers" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/juju/apiserver/websocket/websockettest" 22 "github.com/juju/juju/rpc/params" 23 "github.com/juju/juju/state" 24 coretesting "github.com/juju/juju/testing" 25 "github.com/juju/juju/testing/factory" 26 ) 27 28 type pubsubSuite struct { 29 apiserverBaseSuite 30 machineTag names.Tag 31 password string 32 nonce string 33 hub *pubsub.StructuredHub 34 pubsubURL string 35 } 36 37 var _ = gc.Suite(&pubsubSuite{}) 38 39 func (s *pubsubSuite) SetUpTest(c *gc.C) { 40 s.apiserverBaseSuite.SetUpTest(c) 41 s.nonce = "nonce" 42 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 43 Nonce: s.nonce, 44 Jobs: []state.MachineJob{state.JobManageModel}, 45 }) 46 s.machineTag = m.Tag() 47 s.password = password 48 s.hub = s.config.Hub 49 50 address := s.server.Listener.Addr().String() 51 path := fmt.Sprintf("/model/%s/pubsub", s.State.ModelUUID()) 52 pubsubURL := &url.URL{ 53 Scheme: "wss", 54 Host: address, 55 Path: path, 56 } 57 s.pubsubURL = pubsubURL.String() 58 } 59 60 func (s *pubsubSuite) TestNoAuth(c *gc.C) { 61 s.checkAuthFails(c, nil, http.StatusUnauthorized, "authentication failed: no credentials provided") 62 } 63 64 func (s *pubsubSuite) TestRejectsUserLogins(c *gc.C) { 65 user := s.Factory.MakeUser(c, &factory.UserParams{Password: "sekrit"}) 66 header := jujuhttp.BasicAuthHeader(user.Tag().String(), "sekrit") 67 s.checkAuthFails(c, header, http.StatusForbidden, "authorization failed: user username-.* is not a controller") 68 } 69 70 func (s *pubsubSuite) TestRejectsNonServerMachineLogins(c *gc.C) { 71 m, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 72 Nonce: "a-nonce", 73 Jobs: []state.MachineJob{state.JobHostUnits}, 74 }) 75 header := jujuhttp.BasicAuthHeader(m.Tag().String(), password) 76 header.Add(params.MachineNonceHeader, "a-nonce") 77 s.checkAuthFails(c, header, http.StatusForbidden, "authorization failed: machine .* is not a controller") 78 } 79 80 func (s *pubsubSuite) TestRejectsBadPassword(c *gc.C) { 81 header := jujuhttp.BasicAuthHeader(s.machineTag.String(), "wrong") 82 header.Add(params.MachineNonceHeader, s.nonce) 83 s.checkAuthFails(c, header, http.StatusUnauthorized, "authentication failed: invalid entity name or password") 84 } 85 86 func (s *pubsubSuite) TestRejectsIncorrectNonce(c *gc.C) { 87 header := jujuhttp.BasicAuthHeader(s.machineTag.String(), s.password) 88 header.Add(params.MachineNonceHeader, "wrong") 89 s.checkAuthFails(c, header, http.StatusUnauthorized, "authentication failed: machine 0 not provisioned") 90 } 91 92 func (s *pubsubSuite) checkAuthFails(c *gc.C, header http.Header, code int, message string) { 93 conn, resp, err := s.dialWebsocketInternal(c, header) 94 c.Assert(err, gc.Equals, websocket.ErrBadHandshake) 95 c.Assert(conn, gc.IsNil) 96 c.Assert(resp, gc.NotNil) 97 defer resp.Body.Close() 98 c.Check(resp.StatusCode, gc.Equals, code) 99 out, err := io.ReadAll(resp.Body) 100 c.Assert(err, jc.ErrorIsNil) 101 c.Assert(string(out), gc.Matches, message+"\n") 102 } 103 104 func (s *pubsubSuite) TestMessage(c *gc.C) { 105 messages := []params.PubSubMessage{} 106 done := make(chan struct{}) 107 loggo.GetLogger("pubsub").SetLogLevel(loggo.TRACE) 108 loggo.GetLogger("juju.apiserver").SetLogLevel(loggo.TRACE) 109 _, err := s.hub.SubscribeMatch(pubsub.MatchAll, func(topic string, data map[string]interface{}) { 110 c.Logf("topic: %q, data: %v", topic, data) 111 messages = append(messages, params.PubSubMessage{ 112 Topic: topic, 113 Data: data, 114 }) 115 done <- struct{}{} 116 }) 117 c.Assert(err, jc.ErrorIsNil) 118 119 conn := s.dialWebsocket(c) 120 defer conn.Close() 121 122 // Read back the nil error, indicating that all is well. 123 websockettest.AssertJSONInitialErrorNil(c, conn) 124 125 message1 := params.PubSubMessage{ 126 Topic: "first", 127 Data: map[string]interface{}{ 128 "origin": "other", 129 "message": "first message", 130 }} 131 err = conn.WriteJSON(&message1) 132 c.Assert(err, jc.ErrorIsNil) 133 134 message2 := params.PubSubMessage{ 135 Topic: "second", 136 Data: map[string]interface{}{ 137 "origin": "other", 138 "value": false, 139 }} 140 err = conn.WriteJSON(&message2) 141 c.Assert(err, jc.ErrorIsNil) 142 143 select { 144 case <-done: 145 case <-time.After(coretesting.LongWait): 146 c.Fatalf("no first message") 147 } 148 149 select { 150 case <-done: 151 case <-time.After(coretesting.LongWait): 152 c.Fatalf("no second message") 153 } 154 155 // Close connection. 156 err = conn.Close() 157 c.Assert(err, jc.ErrorIsNil) 158 159 c.Assert(messages, jc.DeepEquals, []params.PubSubMessage{message1, message2}) 160 } 161 162 func (s *pubsubSuite) dialWebsocket(c *gc.C) *websocket.Conn { 163 conn, _, err := s.dialWebsocketInternal(c, s.makeAuthHeader()) 164 c.Assert(err, jc.ErrorIsNil) 165 return conn 166 } 167 168 func (s *pubsubSuite) dialWebsocketInternal(c *gc.C, header http.Header) (*websocket.Conn, *http.Response, error) { 169 return dialWebsocketFromURL(c, s.pubsubURL, header) 170 } 171 172 func (s *pubsubSuite) makeAuthHeader() http.Header { 173 header := jujuhttp.BasicAuthHeader(s.machineTag.String(), s.password) 174 header.Add(params.MachineNonceHeader, s.nonce) 175 return header 176 }