github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/registration_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 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 "strings" 13 14 "github.com/juju/errors" 15 jujuhttp "github.com/juju/http/v2" 16 jc "github.com/juju/testing/checkers" 17 "github.com/juju/testing/httptesting" 18 "go.uber.org/mock/gomock" 19 "golang.org/x/crypto/nacl/secretbox" 20 gc "gopkg.in/check.v1" 21 22 "github.com/juju/juju/apiserver" 23 "github.com/juju/juju/environs" 24 "github.com/juju/juju/rpc/params" 25 "github.com/juju/juju/state" 26 "github.com/juju/juju/state/stateenvirons" 27 coretesting "github.com/juju/juju/testing" 28 ) 29 30 type registrationSuite struct { 31 apiserverBaseSuite 32 bob *state.User 33 registrationURL string 34 } 35 36 var _ = gc.Suite(®istrationSuite{}) 37 38 func (s *registrationSuite) SetUpTest(c *gc.C) { 39 s.apiserverBaseSuite.SetUpTest(c) 40 bob, err := s.State.AddUserWithSecretKey("bob", "", "admin") 41 c.Assert(err, jc.ErrorIsNil) 42 s.bob = bob 43 s.registrationURL = s.server.URL + "/register" 44 } 45 46 func (s *registrationSuite) assertRegisterNoProxy(c *gc.C, hasProxy bool) { 47 ctrl := gomock.NewController(c) 48 defer ctrl.Finish() 49 50 rawConfig := map[string]interface{}{ 51 "api-host": "https://127.0.0.1:16443", 52 "ca-cert": "cert====", 53 "namespace": "controller-k1", 54 "remote-port": "17070", 55 "service": "controller-service", 56 "service-account-token": "token====", 57 } 58 environ := NewMockConnectorInfo(ctrl) 59 proxier := NewMockProxier(ctrl) 60 s.PatchValue(&apiserver.GetConnectorInfoer, func(stateenvirons.Model) (environs.ConnectorInfo, error) { 61 if hasProxy { 62 return environ, nil 63 } 64 return nil, errors.NotSupportedf("proxier") 65 }) 66 if hasProxy { 67 environ.EXPECT().ConnectionProxyInfo().Return(proxier, nil) 68 proxier.EXPECT().RawConfig().Return(rawConfig, nil) 69 proxier.EXPECT().Type().Return("kubernetes-port-forward") 70 } 71 72 // Ensure we cannot log in with the password yet. 73 const password = "hunter2" 74 c.Assert(s.bob.PasswordValid(password), jc.IsFalse) 75 76 validNonce := []byte(strings.Repeat("X", 24)) 77 secretKey := s.bob.SecretKey() 78 ciphertext := s.sealBox( 79 c, validNonce, secretKey, fmt.Sprintf(`{"password": "%s"}`, password), 80 ) 81 client := jujuhttp.NewClient(jujuhttp.WithSkipHostnameVerification(true)) 82 resp := httptesting.Do(c, httptesting.DoRequestParams{ 83 Do: client.Do, 84 URL: s.registrationURL, 85 Method: "POST", 86 JSONBody: ¶ms.SecretKeyLoginRequest{ 87 User: "user-bob", 88 Nonce: validNonce, 89 PayloadCiphertext: ciphertext, 90 }, 91 }) 92 c.Assert(resp.StatusCode, gc.Equals, http.StatusOK) 93 defer resp.Body.Close() 94 95 // It should be possible to log in as bob with the 96 // password "hunter2" now, and there should be no 97 // secret key any longer. 98 err := s.bob.Refresh() 99 c.Assert(err, jc.ErrorIsNil) 100 c.Assert(s.bob.PasswordValid(password), jc.IsTrue) 101 c.Assert(s.bob.SecretKey(), gc.IsNil) 102 103 var response params.SecretKeyLoginResponse 104 bodyData, err := io.ReadAll(resp.Body) 105 c.Assert(err, jc.ErrorIsNil) 106 err = json.Unmarshal(bodyData, &response) 107 c.Assert(err, jc.ErrorIsNil) 108 c.Assert(response.Nonce, gc.HasLen, len(validNonce)) 109 plaintext := s.openBox(c, response.PayloadCiphertext, response.Nonce, secretKey) 110 111 var responsePayload params.SecretKeyLoginResponsePayload 112 err = json.Unmarshal(plaintext, &responsePayload) 113 c.Assert(err, jc.ErrorIsNil) 114 c.Assert(responsePayload.CACert, gc.Equals, coretesting.CACert) 115 model, err := s.State.Model() 116 c.Assert(err, jc.ErrorIsNil) 117 c.Assert(responsePayload.ControllerUUID, gc.Equals, model.ControllerUUID()) 118 if hasProxy { 119 c.Assert(responsePayload.ProxyConfig, gc.DeepEquals, ¶ms.Proxy{ 120 Type: "kubernetes-port-forward", Config: rawConfig, 121 }) 122 } else { 123 c.Assert(responsePayload.ProxyConfig, gc.IsNil) 124 } 125 } 126 127 func (s *registrationSuite) TestRegisterNoProxy(c *gc.C) { 128 s.assertRegisterNoProxy(c, false) 129 } 130 131 func (s *registrationSuite) TestRegisterWithProxy(c *gc.C) { 132 s.assertRegisterNoProxy(c, true) 133 } 134 135 func (s *registrationSuite) TestRegisterInvalidMethod(c *gc.C) { 136 client := jujuhttp.NewClient(jujuhttp.WithSkipHostnameVerification(true)) 137 httptesting.AssertJSONCall(c, httptesting.JSONCallParams{ 138 Do: client.Do, 139 URL: s.registrationURL, 140 Method: "GET", 141 ExpectStatus: http.StatusMethodNotAllowed, 142 ExpectBody: ¶ms.ErrorResult{ 143 Error: ¶ms.Error{ 144 Message: `unsupported method: "GET"`, 145 Code: params.CodeMethodNotAllowed, 146 }, 147 }, 148 }) 149 } 150 151 func (s *registrationSuite) TestRegisterInvalidFormat(c *gc.C) { 152 s.testInvalidRequest( 153 c, "[]", "json: cannot unmarshal array into Go value of type params.SecretKeyLoginRequest", "", 154 http.StatusInternalServerError, 155 ) 156 } 157 158 func (s *registrationSuite) TestRegisterInvalidUserTag(c *gc.C) { 159 s.testInvalidRequest( 160 c, `{"user": "application-bob"}`, `"application-bob" is not a valid user tag`, "", 161 http.StatusInternalServerError, 162 ) 163 } 164 165 func (s *registrationSuite) TestRegisterInvalidNonce(c *gc.C) { 166 s.testInvalidRequest( 167 c, `{"user": "user-bob", "nonce": ""}`, `nonce not valid`, params.CodeNotValid, 168 http.StatusInternalServerError, 169 ) 170 } 171 172 func (s *registrationSuite) TestRegisterInvalidCiphertext(c *gc.C) { 173 validNonce := []byte(strings.Repeat("X", 24)) 174 s.testInvalidRequest(c, 175 fmt.Sprintf( 176 `{"user": "user-bob", "nonce": "%s"}`, 177 base64.StdEncoding.EncodeToString(validNonce), 178 ), `secret key not valid`, params.CodeNotValid, 179 http.StatusInternalServerError, 180 ) 181 } 182 183 func (s *registrationSuite) TestRegisterNoSecretKey(c *gc.C) { 184 err := s.bob.SetPassword("anything") 185 c.Assert(err, jc.ErrorIsNil) 186 validNonce := []byte(strings.Repeat("X", 24)) 187 s.testInvalidRequest(c, 188 fmt.Sprintf( 189 `{"user": "user-bob", "nonce": "%s"}`, 190 base64.StdEncoding.EncodeToString(validNonce), 191 ), `secret key for user "bob" not found`, params.CodeNotFound, 192 http.StatusNotFound, 193 ) 194 } 195 196 func (s *registrationSuite) TestRegisterInvalidRequestPayload(c *gc.C) { 197 validNonce := []byte(strings.Repeat("X", 24)) 198 ciphertext := s.sealBox(c, validNonce, s.bob.SecretKey(), "[]") 199 s.testInvalidRequest(c, 200 fmt.Sprintf( 201 `{"user": "user-bob", "nonce": "%s", "cipher-text": "%s"}`, 202 base64.StdEncoding.EncodeToString(validNonce), 203 base64.StdEncoding.EncodeToString(ciphertext), 204 ), 205 `cannot unmarshal payload: json: cannot unmarshal array into Go value of type params.SecretKeyLoginRequestPayload`, "", 206 http.StatusInternalServerError, 207 ) 208 } 209 210 func (s *registrationSuite) testInvalidRequest(c *gc.C, requestBody, errorMessage, errorCode string, statusCode int) { 211 client := jujuhttp.NewClient(jujuhttp.WithSkipHostnameVerification(true)) 212 httptesting.AssertJSONCall(c, httptesting.JSONCallParams{ 213 Do: client.Do, 214 URL: s.registrationURL, 215 Method: "POST", 216 Body: strings.NewReader(requestBody), 217 ExpectStatus: statusCode, 218 ExpectBody: ¶ms.ErrorResult{ 219 Error: ¶ms.Error{Message: errorMessage, Code: errorCode}, 220 }, 221 }) 222 } 223 224 func (s *registrationSuite) sealBox(c *gc.C, nonce, key []byte, message string) []byte { 225 var nonceArray [24]byte 226 var keyArray [32]byte 227 c.Assert(copy(nonceArray[:], nonce), gc.Equals, len(nonceArray)) 228 c.Assert(copy(keyArray[:], key), gc.Equals, len(keyArray)) 229 return secretbox.Seal(nil, []byte(message), &nonceArray, &keyArray) 230 } 231 232 func (s *registrationSuite) openBox(c *gc.C, ciphertext, nonce, key []byte) []byte { 233 var nonceArray [24]byte 234 var keyArray [32]byte 235 c.Assert(copy(nonceArray[:], nonce), gc.Equals, len(nonceArray)) 236 c.Assert(copy(keyArray[:], key), gc.Equals, len(keyArray)) 237 message, ok := secretbox.Open(nil, ciphertext, &nonceArray, &keyArray) 238 c.Assert(ok, jc.IsTrue) 239 return message 240 }