github.com/argoproj/argo-cd/v3@v3.2.1/util/test/testutil.go (about) 1 package test 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "net/http" 7 "net/http/httptest" 8 "regexp" 9 "testing" 10 "time" 11 12 log "github.com/sirupsen/logrus" 13 14 "github.com/go-jose/go-jose/v4" 15 "github.com/golang-jwt/jwt/v5" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // Cert is a certificate for tests. It was generated like this: 20 // 21 // opts := tls.CertOptions{Hosts: []string{"localhost"}, Organization: "Acme"} 22 // certBytes, privKey, err := tls.generatePEM(opts) 23 var Cert = []byte(`-----BEGIN CERTIFICATE----- 24 MIIC8zCCAdugAwIBAgIQCSoocl6e/FR4mQy1wX6NbjANBgkqhkiG9w0BAQsFADAP 25 MQ0wCwYDVQQKEwRBY21lMB4XDTIyMDYyMjE3Mjk1MloXDTIzMDYyMjE3Mjk1Mlow 26 DzENMAsGA1UEChMEQWNtZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB 27 ANih5Kdn3tEXh6gLfQYplhHnNq8lmSMoPY7wdwXT95sxX9GzrVpR5tRQBExcR+Ie 28 Y2AElGmlhMETTchpU9RoU6fozjAuMYTkm+f0pyNnbdhCE5LnUBSrEhVHSQJ3ajs5 29 I6z9qS+H4uG+yVobiwzt+rnwD+Jdpt7ZwLHhkkkyHHFr8yxRVLN8LBzh8TnCRgj9 30 We64s8ZepkymC/2fhh6jdezibJQ3/dNbj17FHgwmC9oooBj4QwKOpPDzrH26aixu 31 6aAg0yudBS50uahKHI8bfieGYwRFk1PwzhV1mLLc324ZvDmT0KUkhIgQsaYPs47Z 32 EHwsmcVweUUPOAmO/H1ziPUCAwEAAaNLMEkwDgYDVR0PAQH/BAQDAgWgMBMGA1Ud 33 JQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJbG9jYWxo 34 b3N0MA0GCSqGSIb3DQEBCwUAA4IBAQA+8cGJfYRhXQxan7FATsbtC+1DwW1cPc60 35 5eLOuI0jPdvXLDmtOulBEjR4KOfJ5oTKXGjs/+gR3sffP6s8gm2XFQn4+OsmxHbO 36 b2RjPHgKUtJmrI4ZCN8iPGlKIar5u6Q8NZwzpeZ2XL0bpPp7RQsfHqMyhsqDinWR 37 vvwQB+Bri0oIOtzW2645vWmYc2SaFMd8+8g6Ipa+PRSJezeUxIVZG12zlhsio18F 38 9SHY2ONcYISjfrGTIcu4cZRGxCZGTIwMngBlb71mia+K7uH+UE6qfJy/t6KiFsCP 39 yOwMb95nGQSQLDNoGr8gwgE2qPuR0kR9Z5OrWF0DoVCyL3xnxr02 40 -----END CERTIFICATE-----`) 41 42 // PrivateKey is an RSA key used only for tests. 43 var PrivateKey = []byte(`-----BEGIN RSA PRIVATE KEY----- 44 MIIEogIBAAKCAQEA2KHkp2fe0ReHqAt9BimWEec2ryWZIyg9jvB3BdP3mzFf0bOt 45 WlHm1FAETFxH4h5jYASUaaWEwRNNyGlT1GhTp+jOMC4xhOSb5/SnI2dt2EITkudQ 46 FKsSFUdJAndqOzkjrP2pL4fi4b7JWhuLDO36ufAP4l2m3tnAseGSSTIccWvzLFFU 47 s3wsHOHxOcJGCP1Z7rizxl6mTKYL/Z+GHqN17OJslDf901uPXsUeDCYL2iigGPhD 48 Ao6k8POsfbpqLG7poCDTK50FLnS5qEocjxt+J4ZjBEWTU/DOFXWYstzfbhm8OZPQ 49 pSSEiBCxpg+zjtkQfCyZxXB5RQ84CY78fXOI9QIDAQABAoIBAG8jL0FLIp62qZvm 50 uO9ualUo/37/lP7aaCpq50UQJ9lwjS3yNh8+IWQO4QWj2iUBXg4mi1Vf2ymKk78b 51 eixgkXp1D0Lcj/8ToYBwnUami04FKDGXhhf0Y8SS27vuM4vKlqjrQd7modkangYi 52 V0X82UKHDD8fuLpfkGIxzXDLypfMzjMuVpSntnWaf2YX3VR/0/66yEp9GejftF2k 53 wqhGoWM6r68pN5XuCqWd5PRluSoDy/o4BAFMhYCSfp9PjgZE8aoeWHgYzlZ3gUyn 54 r+HaDDNWbibhobXk/9h8lwAJ6KCZ5RZ+HFfh0HuwIxmocT9OCFgy/S0g1p+o3m9K 55 VNd5AMkCgYEA5fbS5UK7FBzuLoLgr1hktmbLJhpt8y8IPHNABHcUdE+O4/1xTQNf 56 pMUwkKjGG1MtrGjLOIoMGURKKn8lR1GMZueOTSKY0+mAWUGvSzl6vwtJwvJruT8M 57 otEO03o0tPnRKGxbFjqxkp2b6iqJ8MxCRZ3lSidc4mdi7PHzv9lwgvsCgYEA8Siq 58 7weCri9N6y+tIdORAXgRzcW54BmJyqB147c72RvbMacb6rN28KXpM3qnRXyp3Llb 59 yh81TW3FH10GqrjATws7BK8lP9kkAw0Z/7kNiS1NgH3pUbO+5H2kAa/6QW35nzRe 60 Jw2lyfYGWqYO4hYXH14ML1kjgS1hgd3XHOQ64M8CgYAKcjDYSzS2UC4dnMJaFLjW 61 dErsGy09a7iDDnUs/r/GHMsP3jZkWi/hCzgOiiwdl6SufUAl/FdaWnjH/2iRGco3 62 7nLPXC/3CFdVNp+g2iaSQRADtAFis9N+HeL/hkCYq/RtUqa8lsP0NgacF3yWnKCy 63 Ct8chDc67ZlXzBHXeCgdOwKBgHHGFPbWXUHeUW1+vbiyvrupsQSanznp8oclMtkv 64 Dk48hSokw9fzuU6Jh77gw9/Vk7HtxS9Tj+squZA1bDrJFPl1u+9WzkUUJZhG6xgp 65 bwhj1iejv5rrKUlVOTYOlwudXeJNa4oTNz9UEeVcaLMjZt9GmIsSC90a0uDZD26z 66 AlAjAoGAEoqm2DcNN7SrH6aVFzj1EVOrNsHYiXj/yefspeiEmf27PSAslP+uF820 67 SDpz4h+Bov5qTKkzcxuu1QWtA4M0K8Iy6IYLwb83DZEm1OsAf4i0pODz21PY/I+O 68 VHzjB10oYgaInHZgMUdyb6F571UdiYSB6a/IlZ3ngj5touy3VIM= 69 -----END RSA PRIVATE KEY-----`) 70 71 // PrivateKey2 is a second RSA key used only for tests. You can use it to see if signing a JWT with a different key 72 // fails validation (as it should). 73 var PrivateKey2 = []byte(`-----BEGIN RSA PRIVATE KEY----- 74 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 111 -----END RSA PRIVATE KEY-----`) 112 113 func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) { 114 t.Helper() 115 return func(w http.ResponseWriter, r *http.Request) { 116 w.Header().Set("Content-Type", "application/json") 117 switch r.RequestURI { 118 case "/api/dex/.well-known/openid-configuration": 119 _, err := fmt.Fprintf(w, ` 120 { 121 "issuer": "%[1]s/api/dex", 122 "authorization_endpoint": "%[1]s/api/dex/auth", 123 "token_endpoint": "%[1]s/api/dex/token", 124 "jwks_uri": "%[1]s/api/dex/keys", 125 "userinfo_endpoint": "%[1]s/api/dex/userinfo", 126 "device_authorization_endpoint": "%[1]s/api/dex/device/code", 127 "grant_types_supported": ["authorization_code"], 128 "response_types_supported": ["code"], 129 "subject_types_supported": ["public"], 130 "id_token_signing_alg_values_supported": ["RS512"], 131 "code_challenge_methods_supported": ["S256", "plain"], 132 "scopes_supported": ["openid"], 133 "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], 134 "claims_supported": ["sub", "aud", "exp"] 135 }`, url) 136 require.NoError(t, err) 137 default: 138 w.WriteHeader(http.StatusNotFound) 139 } 140 } 141 } 142 143 func GetDexTestServer(t *testing.T) *httptest.Server { 144 t.Helper() 145 ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { 146 // Start with a placeholder. We need the server URL before setting up the real handler. 147 })) 148 ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 149 dexMockHandler(t, ts.URL)(w, r) 150 }) 151 return ts 152 } 153 154 func oidcMockHandler(t *testing.T, url string, tokenRequestPreHandler func(r *http.Request)) func(http.ResponseWriter, *http.Request) { 155 t.Helper() 156 return func(w http.ResponseWriter, r *http.Request) { 157 w.Header().Set("Content-Type", "application/json") 158 switch r.RequestURI { 159 case "/.well-known/openid-configuration": 160 _, err := fmt.Fprintf(w, ` 161 { 162 "issuer": "%[1]s", 163 "authorization_endpoint": "%[1]s/auth", 164 "token_endpoint": "%[1]s/token", 165 "jwks_uri": "%[1]s/keys", 166 "userinfo_endpoint": "%[1]s/userinfo", 167 "device_authorization_endpoint": "%[1]s/device/code", 168 "grant_types_supported": ["authorization_code"], 169 "response_types_supported": ["code"], 170 "subject_types_supported": ["public"], 171 "id_token_signing_alg_values_supported": ["RS512"], 172 "code_challenge_methods_supported": ["S256", "plain"], 173 "scopes_supported": ["openid"], 174 "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], 175 "claims_supported": ["sub", "aud", "exp"] 176 }`, url) 177 require.NoError(t, err) 178 case "/userinfo": 179 w.Header().Set("content-type", "application/json") 180 _, err := fmt.Fprintf(w, ` 181 { 182 "groups":["githubOrg:engineers"], 183 "iss": "%[1]s", 184 "sub": "randomUser" 185 }`, url) 186 187 require.NoError(t, err) 188 case "/keys": 189 pubKey, err := jwt.ParseRSAPublicKeyFromPEM(Cert) 190 require.NoError(t, err) 191 jwks := jose.JSONWebKeySet{ 192 Keys: []jose.JSONWebKey{ 193 { 194 Key: pubKey, 195 }, 196 }, 197 } 198 out, err := json.Marshal(jwks) 199 require.NoError(t, err) 200 _, err = w.Write(out) 201 require.NoError(t, err) 202 case "/token": 203 if tokenRequestPreHandler != nil { 204 tokenRequestPreHandler(r) 205 } 206 response, err := mockTokenEndpointResponse(url) 207 require.NoError(t, err) 208 out, err := json.Marshal(response) 209 require.NoError(t, err) 210 _, err = w.Write(out) 211 require.NoError(t, err) 212 default: 213 w.WriteHeader(http.StatusNotFound) 214 } 215 } 216 } 217 218 func GetOIDCTestServer(t *testing.T, tokenRequestPreHandler func(r *http.Request)) *httptest.Server { 219 t.Helper() 220 ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { 221 // Start with a placeholder. We need the server URL before setting up the real handler. 222 })) 223 ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 224 oidcMockHandler(t, ts.URL, tokenRequestPreHandler)(w, r) 225 }) 226 return ts 227 } 228 229 func GetAzureOIDCTestServer(t *testing.T, tokenRequestPreHandler func(r *http.Request)) *httptest.Server { 230 t.Helper() 231 ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { 232 // Start with a placeholder. We need the server URL before setting up the real handler. 233 })) 234 ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 235 oidcMockHandler(t, ts.URL, tokenRequestPreHandler)(w, r) 236 }) 237 return ts 238 } 239 240 type TokenResponse struct { 241 AccessToken string `json:"access_token"` 242 TokenType string `json:"token_type"` 243 ExpiresIn int `json:"expires_in"` 244 IDToken string `json:"id_token"` 245 RefreshToken string `json:"refresh_token"` 246 } 247 248 func mockTokenEndpointResponse(issuer string) (TokenResponse, error) { 249 token, err := generateJWTToken(issuer) 250 return TokenResponse{ 251 AccessToken: token, 252 TokenType: "Bearer", 253 ExpiresIn: 3600, 254 IDToken: token, 255 RefreshToken: token, 256 }, err 257 } 258 259 // Helper function to generate a JWT token 260 func generateJWTToken(issuer string) (string, error) { 261 token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.MapClaims{ 262 "sub": "1234567890", 263 "aud": "test-client-id", 264 "name": "John Doe", 265 "iat": time.Now().Unix(), 266 "iss": issuer, 267 "exp": time.Now().Add(time.Hour).Unix(), // Set the expiration time 268 }) 269 key, err := jwt.ParseRSAPrivateKeyFromPEM(PrivateKey) 270 if err != nil { 271 return "", fmt.Errorf("failed to parse RSA private key: %w", err) 272 } 273 tokenString, err := token.SignedString(key) 274 if err != nil { 275 return "", err 276 } 277 return tokenString, nil 278 } 279 280 type LogHook struct { 281 Entries []log.Entry 282 } 283 284 func (h *LogHook) Levels() []log.Level { 285 return []log.Level{log.WarnLevel} 286 } 287 288 func (h *LogHook) Fire(entry *log.Entry) error { 289 h.Entries = append(h.Entries, *entry) 290 return nil 291 } 292 293 func (h *LogHook) GetRegexMatchesInEntries(match string) []string { 294 re := regexp.MustCompile(match) 295 matches := make([]string, 0) 296 for _, entry := range h.Entries { 297 if re.MatchString(entry.Message) { 298 matches = append(matches, entry.Message) 299 } 300 } 301 return matches 302 }