github.com/pelicanplatform/pelican@v1.0.5/web_ui/prometheus_test.go (about)

     1  /***************************************************************
     2   *
     3   * Copyright (C) 2023, Pelican Project, Morgridge Institute for Research
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License"); you
     6   * may not use this file except in compliance with the License.  You may
     7   * obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   ***************************************************************/
    18  package web_ui
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/ecdsa"
    23  	"crypto/elliptic"
    24  	"crypto/rand"
    25  	"encoding/base64"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"path/filepath"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/gin-gonic/gin"
    34  	"github.com/lestrrat-go/jwx/v2/jwa"
    35  	"github.com/lestrrat-go/jwx/v2/jwk"
    36  	"github.com/lestrrat-go/jwx/v2/jwt"
    37  	"github.com/pelicanplatform/pelican/config"
    38  	"github.com/pelicanplatform/pelican/param"
    39  	"github.com/prometheus/common/route"
    40  	"github.com/spf13/viper"
    41  	"github.com/stretchr/testify/assert"
    42  )
    43  
    44  func TestPrometheusProtectionFederationURL(t *testing.T) {
    45  
    46  	/*
    47  	* Tests that prometheus metrics are behind federation's token. Specifically it signs a token
    48  	* with the a generated key o prometheus GET endpoint with both URL. It mimics matching the Federation URL
    49  	* to ensure that check is done, but intercepts with returning a generated jwk for testing purposes
    50  	 */
    51  
    52  	// Setup httptest recorder and context for the the unit test
    53  	viper.Reset()
    54  
    55  	av1 := route.New().WithPrefix("/api/v1.0/prometheus")
    56  
    57  	// Create temp dir for the origin key file
    58  	tDir := t.TempDir()
    59  	kfile := filepath.Join(tDir, "testKey")
    60  	//Setup a private key
    61  	viper.Set("IssuerKey", kfile)
    62  
    63  	w := httptest.NewRecorder()
    64  	c, r := gin.CreateTestContext(w)
    65  
    66  	// Set ExternalWebUrl so that IssuerCheck can pass
    67  	viper.Set("Server.ExternalWebUrl", "https://test-origin.org:8444")
    68  
    69  	c.Request = &http.Request{
    70  		URL: &url.URL{},
    71  	}
    72  
    73  	jti_bytes := make([]byte, 16)
    74  	_, err := rand.Read(jti_bytes)
    75  	if err != nil {
    76  		t.Fatal(err)
    77  	}
    78  	jti := base64.RawURLEncoding.EncodeToString(jti_bytes)
    79  
    80  	issuerUrl := param.Server_ExternalWebUrl.GetString()
    81  	tok, err := jwt.NewBuilder().
    82  		Claim("scope", "monitoring.query").
    83  		Claim("wlcg.ver", "1.0").
    84  		JwtID(jti).
    85  		Issuer(issuerUrl).
    86  		Audience([]string{issuerUrl}).
    87  		Subject("sub").
    88  		Expiration(time.Now().Add(time.Minute)).
    89  		IssuedAt(time.Now()).
    90  		Build()
    91  
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  
    96  	pkey, err := config.GetIssuerPrivateJWK()
    97  	if err != nil {
    98  		t.Fatal(err)
    99  	}
   100  
   101  	// Sign the token with the origin private key
   102  	signed, err := jwt.Sign(tok, jwt.WithKey(jwa.ES256, pkey))
   103  
   104  	if err != nil {
   105  		t.Fatal(err)
   106  	}
   107  
   108  	// Set the request to run through the promQueryEngineAuthHandler function
   109  	r.GET("/api/v1.0/prometheus/*any", promQueryEngineAuthHandler(av1))
   110  	c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/test", bytes.NewBuffer([]byte(`{}`)))
   111  
   112  	// Puts the token in cookie
   113  	c.Request.AddCookie(&http.Cookie{Name: "login", Value: string(signed)})
   114  
   115  	r.ServeHTTP(w, c.Request)
   116  
   117  	assert.Equal(t, 404, w.Result().StatusCode, "Expected status code of 404 representing failure due to minimal server setup, not token check")
   118  }
   119  
   120  func TestPrometheusProtectionOriginHeaderScope(t *testing.T) {
   121  	/*
   122  	* Tests that the prometheus protections are behind the origin's token and tests that the token is accessable from
   123  	* the header function. It signs a token with the origin's jwks key and adds it to the header before attempting
   124  	* to access the prometheus metrics. It then attempts to access the metrics with a token with an invalid scope.
   125  	* It attempts to do so again with a token signed by a bad key. Both these are expected to fail.
   126  	 */
   127  
   128  	viper.Reset()
   129  	viper.Set("Server.ExternalWebUrl", "https://test-origin.org:8444")
   130  
   131  	av1 := route.New().WithPrefix("/api/v1.0/prometheus")
   132  
   133  	// Create temp dir for the origin key file
   134  	tDir := t.TempDir()
   135  	kfile := filepath.Join(tDir, "testKey")
   136  
   137  	//Setup a private key and a token
   138  	viper.Set("IssuerKey", kfile)
   139  
   140  	w := httptest.NewRecorder()
   141  	c, r := gin.CreateTestContext(w)
   142  
   143  	c.Request = &http.Request{
   144  		URL: &url.URL{},
   145  	}
   146  
   147  	// Load the private key
   148  	privKey, err := config.GetIssuerPrivateJWK()
   149  	if err != nil {
   150  		t.Fatal(err)
   151  	}
   152  
   153  	// Create a token
   154  	jti_bytes := make([]byte, 16)
   155  	_, err = rand.Read(jti_bytes)
   156  	if err != nil {
   157  		t.Fatal(err)
   158  	}
   159  	jti := base64.RawURLEncoding.EncodeToString(jti_bytes)
   160  
   161  	issuerUrl := param.Server_ExternalWebUrl.GetString()
   162  	tok, err := jwt.NewBuilder().
   163  		Claim("scope", "monitoring.query").
   164  		Claim("wlcg.ver", "1.0").
   165  		JwtID(jti).
   166  		Issuer(issuerUrl).
   167  		Audience([]string{issuerUrl}).
   168  		Subject("sub").
   169  		Expiration(time.Now().Add(time.Minute)).
   170  		IssuedAt(time.Now()).
   171  		Build()
   172  
   173  	if err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	// Sign the token with the origin private key
   178  	signed, err := jwt.Sign(tok, jwt.WithKey(jwa.ES256, privKey))
   179  	if err != nil {
   180  		t.Fatal(err)
   181  	}
   182  
   183  	// Set the request to go through the promQueryEngineAuthHandler function
   184  	r.GET("/api/v1.0/prometheus/*any", promQueryEngineAuthHandler(av1))
   185  	c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/test", bytes.NewBuffer([]byte(`{}`)))
   186  
   187  	// Put the signed token within the header
   188  	c.Request.Header.Set("Authorization", "Bearer "+string(signed))
   189  	c.Request.Header.Set("Content-Type", "application/json")
   190  
   191  	r.ServeHTTP(w, c.Request)
   192  
   193  	assert.Equal(t, 404, w.Result().StatusCode, "Expected status code of 404 representing failure due to minimal server setup, not token check")
   194  
   195  	// Create a new Recorder and Context for the next HTTPtest call
   196  	w = httptest.NewRecorder()
   197  	c, r = gin.CreateTestContext(w)
   198  
   199  	c.Request = &http.Request{
   200  		URL: &url.URL{},
   201  	}
   202  
   203  	// Create a private key to use for the test
   204  	privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
   205  	assert.NoError(t, err, "Error generating private key")
   206  
   207  	// Convert from raw ecdsa to jwk.Key
   208  	pKey, err := jwk.FromRaw(privateKey)
   209  	assert.NoError(t, err, "Unable to convert ecdsa.PrivateKey to jwk.Key")
   210  
   211  	//Assign Key id to the private key
   212  	err = jwk.AssignKeyID(pKey)
   213  	assert.NoError(t, err, "Error assigning kid to private key")
   214  
   215  	//Set an algorithm for the key
   216  	err = pKey.Set(jwk.AlgorithmKey, jwa.ES256)
   217  	assert.NoError(t, err, "Unable to set algorithm for pKey")
   218  
   219  	jti_bytes = make([]byte, 16)
   220  	_, err = rand.Read(jti_bytes)
   221  	if err != nil {
   222  		t.Fatal(err)
   223  	}
   224  	jti = base64.RawURLEncoding.EncodeToString(jti_bytes)
   225  
   226  	// Create a new token to be used
   227  	tok, err = jwt.NewBuilder().
   228  		Claim("scope", "monitoring.query").
   229  		Claim("wlcg.ver", "1.0").
   230  		JwtID(jti).
   231  		Issuer(issuerUrl).
   232  		Audience([]string{issuerUrl}).
   233  		Subject("sub").
   234  		Expiration(time.Now().Add(time.Minute)).
   235  		IssuedAt(time.Now()).
   236  		Build()
   237  
   238  	assert.NoError(t, err, "Error creating token")
   239  
   240  	// Sign token with private key (not the origin)
   241  	signed, err = jwt.Sign(tok, jwt.WithKey(jwa.ES256, pKey))
   242  	assert.NoError(t, err, "Error signing token")
   243  
   244  	r.GET("/api/v1.0/prometheus/*any", promQueryEngineAuthHandler(av1))
   245  	c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/test", bytes.NewBuffer([]byte(`{}`)))
   246  
   247  	c.Request.Header.Set("Authorization", "Bearer "+string(signed))
   248  	c.Request.Header.Set("Content-Type", "application/json")
   249  
   250  	r.ServeHTTP(w, c.Request)
   251  	// Assert that it gets the correct Permission Denied 403 code
   252  	assert.Equal(t, 403, w.Result().StatusCode, "Expected failing status code of 403: Permission Denied")
   253  
   254  	// Create a new Recorder and Context for the next HTTPtest call
   255  	w = httptest.NewRecorder()
   256  	c, r = gin.CreateTestContext(w)
   257  
   258  	c.Request = &http.Request{
   259  		URL: &url.URL{},
   260  	}
   261  
   262  	// Create a new token to be used
   263  	tok, err = jwt.NewBuilder().
   264  		Claim("scope", "not.prometheus").
   265  		Claim("wlcg.ver", "1.0").
   266  		JwtID(jti).
   267  		Issuer(issuerUrl).
   268  		Audience([]string{issuerUrl}).
   269  		Subject("sub").
   270  		Expiration(time.Now().Add(time.Minute)).
   271  		IssuedAt(time.Now()).
   272  		Build()
   273  
   274  	if err != nil {
   275  		t.Fatal(err)
   276  	}
   277  
   278  	// Sign the token with the origin private key
   279  	signed, err = jwt.Sign(tok, jwt.WithKey(jwa.ES256, privKey))
   280  	if err != nil {
   281  		t.Fatal(err)
   282  	}
   283  
   284  	// Set the request to go through the promQueryEngineAuthHandler function
   285  	r.GET("/api/v1.0/prometheus/*any", promQueryEngineAuthHandler(av1))
   286  	c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/test", bytes.NewBuffer([]byte(`{}`)))
   287  
   288  	// Put the signed token within the header
   289  	c.Request.Header.Set("Authorization", "Bearer "+string(signed))
   290  	c.Request.Header.Set("Content-Type", "application/json")
   291  
   292  	r.ServeHTTP(w, c.Request)
   293  
   294  	assert.Equal(t, 403, w.Result().StatusCode, "Expected status code of 403 due to bad token scope")
   295  
   296  	key, err := config.GetIssuerPrivateJWK()
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  
   301  	// Create a new Recorder and Context for the next HTTPtest call
   302  	w = httptest.NewRecorder()
   303  	c, r = gin.CreateTestContext(w)
   304  
   305  	now := time.Now()
   306  	tok, err = jwt.NewBuilder().
   307  		Issuer(issuerUrl).
   308  		Claim("scope", "monitoring.query").
   309  		Claim("wlcg.ver", "1.0").
   310  		IssuedAt(now).
   311  		Expiration(now.Add(30 * time.Minute)).
   312  		NotBefore(now).
   313  		Subject("user").
   314  		Build()
   315  	if err != nil {
   316  		t.Fatal(err)
   317  	}
   318  
   319  	var raw ecdsa.PrivateKey
   320  	if err = key.Raw(&raw); err != nil {
   321  		t.Fatal(err)
   322  	}
   323  	signed, err = jwt.Sign(tok, jwt.WithKey(jwa.ES256, raw))
   324  	if err != nil {
   325  		t.Fatal(err)
   326  	}
   327  
   328  	// Set the request to go through the promQueryEngineAuthHandler function
   329  	r.GET("/api/v1.0/prometheus/*any", promQueryEngineAuthHandler(av1))
   330  
   331  	http.SetCookie(w, &http.Cookie{Name: "login", Value: string(signed)})
   332  	if err != nil {
   333  		t.Fatal(err)
   334  	}
   335  
   336  	c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/test", bytes.NewBuffer([]byte(`{}`)))
   337  	c.Request.Header.Set("Cookie", w.Header().Get("Set-Cookie"))
   338  
   339  	r.ServeHTTP(w, c.Request)
   340  
   341  	assert.Equal(t, 404, w.Result().StatusCode, "Expected status code of 404 representing failure due to minimal server setup, not token check")
   342  }