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  }