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  }