github.com/pelicanplatform/pelican@v1.0.5/web_ui/ui_test.go (about) 1 //go:build !windows 2 3 /*************************************************************** 4 * 5 * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research 6 * 7 * Licensed under the Apache License, Version 2.0 (the "License"); you 8 * may not use this file except in compliance with the License. You may 9 * obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, software 14 * distributed under the License is distributed on an "AS IS" BASIS, 15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 * See the License for the specific language governing permissions and 17 * limitations under the License. 18 * 19 ***************************************************************/ 20 21 package web_ui 22 23 import ( 24 "context" 25 "crypto/elliptic" 26 "fmt" 27 "math/rand" 28 "net/http" 29 "net/http/httptest" 30 "os" 31 "path/filepath" 32 "strings" 33 "testing" 34 "time" 35 36 "github.com/gin-gonic/gin" 37 "github.com/pelicanplatform/pelican/config" 38 "github.com/pelicanplatform/pelican/param" 39 "github.com/spf13/viper" 40 "github.com/stretchr/testify/assert" 41 "github.com/stretchr/testify/require" 42 ) 43 44 var ( 45 tempPasswdFile *os.File 46 router *gin.Engine 47 ) 48 49 func TestMain(m *testing.M) { 50 gin.SetMode(gin.TestMode) 51 52 //set a temporary password file: 53 tempFile, err := os.CreateTemp("", "web-ui-passwd") 54 if err != nil { 55 fmt.Println("Failed to setup web-ui-passwd file") 56 os.Exit(1) 57 } 58 tempPasswdFile = tempFile 59 //Override viper default for testing 60 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 61 62 //Make a testing issuer.jwk file to get a cookie 63 tempJWKDir, err := os.MkdirTemp("", "tempDir") 64 if err != nil { 65 fmt.Println("Error making temp jwk dir") 66 os.Exit(1) 67 } 68 69 //Override viper default for testing 70 viper.Set("IssuerKey", filepath.Join(tempJWKDir, "issuer.jwk")) 71 72 // Ensure we load up the default configs. 73 config.InitConfig() 74 if err := config.InitServer(config.OriginType); err != nil { 75 fmt.Println("Failed to configure the test module") 76 os.Exit(1) 77 } 78 79 //Get keys 80 _, err = config.GetIssuerPublicJWKS() 81 if err != nil { 82 fmt.Println("Error issuing jwks") 83 os.Exit(1) 84 } 85 router = gin.Default() 86 87 //Configure Web API 88 err = ConfigureServerWebAPI(router, false) 89 if err != nil { 90 fmt.Println("Error configuring web UI") 91 os.Exit(1) 92 } 93 //Run the tests 94 exitCode := m.Run() 95 96 //Clean up created files by removing them and exit 97 os.Remove(tempPasswdFile.Name()) 98 os.RemoveAll(tempJWKDir) 99 os.Exit(exitCode) 100 } 101 102 func TestWaitUntilLogin(t *testing.T) { 103 dirName := t.TempDir() 104 viper.Reset() 105 viper.Set("ConfigDir", dirName) 106 config.InitConfig() 107 err := config.InitServer(config.OriginType) 108 require.NoError(t, err) 109 ctx, cancel := context.WithCancel(context.Background()) 110 defer cancel() 111 go func() { 112 err := waitUntilLogin(ctx) 113 require.NoError(t, err) 114 }() 115 activationCodeFile := param.Server_UIActivationCodeFile.GetString() 116 start := time.Now() 117 for { 118 time.Sleep(10 * time.Millisecond) 119 contents, err := os.ReadFile(activationCodeFile) 120 if os.IsNotExist(err) { 121 if time.Since(start) > 10*time.Second { 122 require.Fail(t, "The UI activation code file did not appear within 10 seconds") 123 } 124 continue 125 } else { 126 require.NoError(t, err) 127 } 128 contentsStr := string(contents[:len(contents)-1]) 129 require.Equal(t, *currentCode.Load(), contentsStr) 130 break 131 } 132 cancel() 133 start = time.Now() 134 for { 135 time.Sleep(10 * time.Millisecond) 136 if _, err := os.Stat(activationCodeFile); err == nil { 137 if time.Since(start) > 10*time.Second { 138 require.Fail(t, "The UI activation code file was not cleaned up") 139 return 140 } 141 continue 142 } else if !os.IsNotExist(err) { 143 require.NoError(t, err) 144 } 145 break 146 } 147 } 148 149 func TestCodeBasedLogin(t *testing.T) { 150 dirName := t.TempDir() 151 viper.Reset() 152 viper.Set("ConfigDir", dirName) 153 config.InitConfig() 154 err := config.InitServer(config.OriginType) 155 require.NoError(t, err) 156 err = config.GeneratePrivateKey(param.IssuerKey.GetString(), elliptic.P256()) 157 require.NoError(t, err) 158 159 //Invoke the code login API with the correct code, ensure we get a valid code back 160 t.Run("With valid code", func(t *testing.T) { 161 newCode := fmt.Sprintf("%06v", rand.Intn(1000000)) 162 currentCode.Store(&newCode) 163 req, err := http.NewRequest("POST", "/api/v1.0/auth/initLogin", strings.NewReader(fmt.Sprintf(`{"code": "%s"}`, newCode))) 164 assert.NoError(t, err) 165 166 req.Header.Set("Content-Type", "application/json") 167 168 recorder := httptest.NewRecorder() 169 router.ServeHTTP(recorder, req) 170 171 //Check the HTTP response code 172 assert.Equal(t, 200, recorder.Code) 173 //Check that we get a cookie back 174 cookies := recorder.Result().Cookies() 175 foundCookie := false 176 for _, cookie := range cookies { 177 if cookie.Name == "login" { 178 foundCookie = true 179 } 180 } 181 assert.True(t, foundCookie) 182 }) 183 184 //Invoke the code login with the wrong code, ensure we get a 401 185 t.Run("With invalid code", func(t *testing.T) { 186 require.True(t, param.Origin_EnableUI.GetBool()) 187 req, err := http.NewRequest("POST", "/api/v1.0/auth/initLogin", strings.NewReader(`{"code": "20"}`)) 188 assert.NoError(t, err) 189 190 req.Header.Set("Content-Type", "application/json") 191 192 recorder := httptest.NewRecorder() 193 router.ServeHTTP(recorder, req) 194 195 //Check the HTTP response code 196 assert.Equal(t, 401, recorder.Code) 197 assert.JSONEq(t, `{"error":"Invalid login code"}`, recorder.Body.String()) 198 }) 199 } 200 201 func TestPasswordResetAPI(t *testing.T) { 202 dirName := t.TempDir() 203 viper.Reset() 204 viper.Set("ConfigDir", dirName) 205 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 206 err := config.InitServer(config.OriginType) 207 require.NoError(t, err) 208 err = config.GeneratePrivateKey(param.IssuerKey.GetString(), elliptic.P256()) 209 require.NoError(t, err) 210 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 211 212 //////////////////////////////SETUP//////////////////////////////// 213 //Add an admin user to file to configure 214 content := "admin:password\n" 215 _, err = tempPasswdFile.WriteString(content) 216 assert.NoError(t, err, "Error writing to temp password file") 217 218 //Configure UI 219 err = configureAuthDB() 220 assert.NoError(t, err) 221 222 //Create a user for testing 223 err = WritePasswordEntry("user", "password") 224 assert.NoError(t, err, "error writing a user") 225 password := "password" 226 user := "user" 227 payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, user, password) 228 229 //Create a request 230 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 231 assert.NoError(t, err) 232 233 req.Header.Set("Content-Type", "application/json") 234 235 recorder := httptest.NewRecorder() 236 router.ServeHTTP(recorder, req) 237 238 //Check ok http reponse 239 assert.Equal(t, http.StatusOK, recorder.Code) 240 //Check that success message returned 241 require.JSONEq(t, `{"msg":"Success"}`, recorder.Body.String()) 242 //Get the cookie to pass to password reset 243 loginCookie := recorder.Result().Cookies() 244 cookieValue := loginCookie[0].Value 245 246 /////////////////////////////////////////////////////////////////// 247 //Test invoking reset with valid authorization 248 t.Run("With valid authorization", func(t *testing.T) { 249 resetPayload := `{"password": "newpassword"}` 250 reqReset, err := http.NewRequest("POST", "/api/v1.0/auth/resetLogin", strings.NewReader(resetPayload)) 251 assert.NoError(t, err) 252 253 reqReset.Header.Set("Content-Type", "application/json") 254 255 reqReset.AddCookie(&http.Cookie{ 256 Name: "login", 257 Value: cookieValue, 258 }) 259 260 recorderReset := httptest.NewRecorder() 261 router.ServeHTTP(recorderReset, reqReset) 262 263 //Check ok http reponse 264 assert.Equal(t, 200, recorderReset.Code) 265 //Check that success message returned 266 assert.JSONEq(t, `{"msg":"Success"}`, recorderReset.Body.String()) 267 268 //After password reset, test authorization with newly generated password 269 loginWithNewPasswordPayload := `{"user": "user", "password": "newpassword"}` 270 271 reqLoginWithNewPassword, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(loginWithNewPasswordPayload)) 272 assert.NoError(t, err) 273 274 reqLoginWithNewPassword.Header.Set("Content-Type", "application/json") 275 276 recorderLoginWithNewPassword := httptest.NewRecorder() 277 router.ServeHTTP(recorderLoginWithNewPassword, reqLoginWithNewPassword) 278 279 //Check HTTP response code 200 280 assert.Equal(t, http.StatusOK, recorderLoginWithNewPassword.Code) 281 282 //Check that the response body contains the success message 283 assert.JSONEq(t, `{"msg":"Success"}`, recorderLoginWithNewPassword.Body.String()) 284 }) 285 286 //Invoking password reset without a cookie should result in failure 287 t.Run("Without valid cookie", func(t *testing.T) { 288 resetPayload := `{"password": "newpassword"}` 289 reqReset, err := http.NewRequest("POST", "/api/v1.0/auth/resetLogin", strings.NewReader(resetPayload)) 290 assert.NoError(t, err) 291 292 reqReset.Header.Set("Content-Type", "application/json") 293 294 recorderReset := httptest.NewRecorder() 295 router.ServeHTTP(recorderReset, reqReset) 296 297 //Check ok http reponse 298 assert.Equal(t, 401, recorderReset.Code) 299 //Check that success message returned 300 assert.JSONEq(t, `{"error":"Authentication required to perform this operation"}`, recorderReset.Body.String()) 301 }) 302 303 } 304 305 func TestPasswordBasedLoginAPI(t *testing.T) { 306 viper.Reset() 307 config.InitConfig() 308 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 309 err := config.InitServer(config.OriginType) 310 require.NoError(t, err) 311 312 ///////////////////////////SETUP/////////////////////////////////// 313 //Add an admin user to file to configure 314 content := "admin:password\n" 315 _, err = tempPasswdFile.WriteString(content) 316 assert.NoError(t, err, "Error writing to temp password file") 317 318 //Configure UI 319 err = configureAuthDB() 320 assert.NoError(t, err) 321 322 //Create a user for testing 323 err = WritePasswordEntry("user", "password") 324 assert.NoError(t, err, "error writing a user") 325 password := "password" 326 user := "user" 327 /////////////////////////////////////////////////////////////////// 328 329 //Invoke with valid password, should get a cookie back 330 t.Run("Successful Login", func(t *testing.T) { 331 payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, user, password) 332 333 //Create a request 334 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 335 assert.NoError(t, err) 336 337 req.Header.Set("Content-Type", "application/json") 338 339 recorder := httptest.NewRecorder() 340 router.ServeHTTP(recorder, req) 341 //Check ok http reponse 342 assert.Equal(t, http.StatusOK, recorder.Code) 343 //Check that success message returned 344 assert.JSONEq(t, `{"msg":"Success"}`, recorder.Body.String()) 345 //Check for a cookie being returned 346 cookies := recorder.Result().Cookies() 347 foundCookie := false 348 for _, cookie := range cookies { 349 if cookie.Name == "login" { 350 foundCookie = true 351 } 352 } 353 assert.True(t, foundCookie) 354 }) 355 356 //Invoke without a password should fail 357 t.Run("Without password", func(t *testing.T) { 358 payload := fmt.Sprintf(`{"user": "%s"}`, user) 359 //Create a request 360 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 361 assert.NoError(t, err) 362 req.Header.Set("Content-Type", "application/json") 363 364 recorder := httptest.NewRecorder() 365 router.ServeHTTP(recorder, req) 366 //Check http reponse code 401 367 assert.Equal(t, 401, recorder.Code) 368 assert.JSONEq(t, `{"error":"Login failed"}`, recorder.Body.String()) 369 }) 370 371 //Invoke with incorrect password should fail 372 t.Run("With incorrect password", func(t *testing.T) { 373 payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, user, "incorrectpassword") 374 //Create a request 375 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 376 assert.NoError(t, err) 377 req.Header.Set("Content-Type", "application/json") 378 379 recorder := httptest.NewRecorder() 380 router.ServeHTTP(recorder, req) 381 //Check http reponse code 401 382 assert.Equal(t, 401, recorder.Code) 383 assert.JSONEq(t, `{"error":"Login failed"}`, recorder.Body.String()) 384 }) 385 386 //Invoke with incorrect user should fail 387 t.Run("With incorrect user", func(t *testing.T) { 388 payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, "incorrectuser", password) 389 //Create a request 390 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 391 assert.NoError(t, err) 392 req.Header.Set("Content-Type", "application/json") 393 394 recorder := httptest.NewRecorder() 395 router.ServeHTTP(recorder, req) 396 //Check http reponse code 401 397 assert.Equal(t, 401, recorder.Code) 398 assert.JSONEq(t, `{"error":"Login failed"}`, recorder.Body.String()) 399 }) 400 401 //Invoke with invalid user, should fail 402 t.Run("Without user", func(t *testing.T) { 403 payload := fmt.Sprintf(`{"password": "%s"}`, password) 404 //Create a request 405 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 406 assert.NoError(t, err) 407 req.Header.Set("Content-Type", "application/json") 408 409 recorder := httptest.NewRecorder() 410 router.ServeHTTP(recorder, req) 411 //Check http reponse code 401 412 assert.Equal(t, 401, recorder.Code) 413 assert.JSONEq(t, `{"error":"Login failed"}`, recorder.Body.String()) 414 }) 415 } 416 417 func TestWhoamiAPI(t *testing.T) { 418 dirName := t.TempDir() 419 viper.Reset() 420 config.InitConfig() 421 viper.Set("ConfigDir", dirName) 422 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 423 err := config.InitServer(config.OriginType) 424 require.NoError(t, err) 425 err = config.GeneratePrivateKey(param.IssuerKey.GetString(), elliptic.P256()) 426 require.NoError(t, err) 427 viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) 428 429 ///////////////////////////SETUP/////////////////////////////////// 430 //Add an admin user to file to configure 431 content := "admin:password\n" 432 _, err = tempPasswdFile.WriteString(content) 433 assert.NoError(t, err, "Error writing to temp password file") 434 435 //Configure UI 436 err = configureAuthDB() 437 assert.NoError(t, err) 438 439 //Create a user for testing 440 err = WritePasswordEntry("user", "password") 441 assert.NoError(t, err, "error writing a user") 442 password := "password" 443 user := "user" 444 payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, user, password) 445 446 //Create a request 447 req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) 448 assert.NoError(t, err) 449 450 req.Header.Set("Content-Type", "application/json") 451 452 recorder := httptest.NewRecorder() 453 router.ServeHTTP(recorder, req) 454 //Check ok http reponse 455 assert.Equal(t, http.StatusOK, recorder.Code) 456 //Check that success message returned 457 assert.JSONEq(t, `{"msg":"Success"}`, recorder.Body.String()) 458 //Get the cookie to test 'whoami' 459 loginCookie := recorder.Result().Cookies() 460 cookieValue := loginCookie[0].Value 461 462 /////////////////////////////////////////////////////////////////// 463 464 //Invoked with valid cookie, should return the username in the cookie 465 t.Run("With valid cookie", func(t *testing.T) { 466 req, err = http.NewRequest("GET", "/api/v1.0/auth/whoami", nil) 467 assert.NoError(t, err) 468 469 req.AddCookie(&http.Cookie{ 470 Name: "login", 471 Value: cookieValue, 472 }) 473 474 recorder = httptest.NewRecorder() 475 router.ServeHTTP(recorder, req) 476 477 //Check for http reponse code 200 478 assert.Equal(t, 200, recorder.Code) 479 assert.JSONEq(t, `{"authenticated":true, "user":"user"}`, recorder.Body.String()) 480 }) 481 //Invoked without valid cookie, should return there is no logged-in user 482 t.Run("Without a valid cookie", func(t *testing.T) { 483 req, err = http.NewRequest("GET", "/api/v1.0/auth/whoami", nil) 484 assert.NoError(t, err) 485 486 recorder = httptest.NewRecorder() 487 router.ServeHTTP(recorder, req) 488 489 //Check for http reponse code 200 490 assert.Equal(t, 200, recorder.Code) 491 assert.JSONEq(t, `{"authenticated":false}`, recorder.Body.String()) 492 }) 493 }