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  }