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 }