github.com/argoproj/argo-cd/v3@v3.2.1/util/dex/dex_test.go (about)

     1  package dex
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"net/url"
     9  	"strings"
    10  	"testing"
    11  
    12  	log "github.com/sirupsen/logrus"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"sigs.k8s.io/yaml"
    16  
    17  	"github.com/argoproj/argo-cd/v3/common"
    18  	utillog "github.com/argoproj/argo-cd/v3/util/log"
    19  	"github.com/argoproj/argo-cd/v3/util/settings"
    20  )
    21  
    22  const invalidURL = ":://localhost/foo/bar"
    23  
    24  var malformedDexConfig = `
    25  valid:
    26    yaml: valid
    27  yaml:
    28    valid
    29  `
    30  
    31  var goodDexConfig = `
    32  connectors:
    33  # GitHub example
    34  - type: github
    35    id: github
    36    name: GitHub
    37    config:
    38      clientID: aabbccddeeff00112233
    39      clientSecret: $dex.github.clientSecret
    40      orgs:
    41      - name: your-github-org
    42  
    43  # GitHub enterprise example
    44  - type: github
    45    id: acme-github
    46    name: Acme GitHub
    47    config:
    48      hostName: github.acme.example.com
    49      clientID: abcdefghijklmnopqrst
    50      clientSecret: $dex.acme.clientSecret
    51      orgs:
    52      - name: your-github-org
    53  `
    54  
    55  var customStaticClientDexConfig = `
    56  connectors:
    57  # GitHub example
    58  - type: github
    59    id: github
    60    name: GitHub
    61    config:
    62      clientID: aabbccddeeff00112233
    63      clientSecret: abcdefghijklmnopqrst\n\r
    64      orgs:
    65      - name: your-github-org
    66  staticClients:
    67  - id: argo-workflow
    68    name: Argo Workflow
    69    redirectURIs:
    70    - https://argo/oauth2/callback
    71    secret:  $dex.acme.clientSecret
    72  `
    73  
    74  var badDexConfig = `
    75  connectors:
    76  # GitHub example
    77  - type: github
    78    id: github
    79    name: GitHub
    80    config: foo
    81  
    82  # GitHub enterprise example
    83  - type: github
    84    id: acme-github
    85    name: Acme GitHub
    86    config:
    87      hostName: github.acme.example.com
    88      clientID: abcdefghijklmnopqrst
    89      clientSecret: $dex.acme.clientSecret
    90      orgs:
    91      - name: your-github-org
    92  `
    93  
    94  var goodDexConfigWithOauthOverrides = `
    95  oauth2:
    96    passwordConnector: ldap
    97  connectors:
    98  - type: ldap
    99    name: OpenLDAP
   100    id: ldap
   101    config:
   102      host: localhost:389
   103      insecureNoSSL: true
   104      bindDN: cn=admin,dc=example,dc=org
   105      bindPW: admin
   106      usernamePrompt: Email Address
   107      userSearch:
   108        baseDN: ou=People,dc=example,dc=org
   109        filter: "(objectClass=person)"
   110        username: mail
   111        idAttr: DN
   112        emailAttr: mail
   113        nameAttr: cn
   114      groupSearch:
   115        baseDN: ou=Groups,dc=example,dc=org
   116        filter: "(objectClass=groupOfNames)"
   117        nameAttr: cn
   118  `
   119  
   120  var goodDexConfigWithEnabledApprovalScreen = `
   121  oauth2:
   122    passwordConnector: ldap
   123    skipApprovalScreen: false
   124  connectors:
   125  - type: ldap
   126    name: OpenLDAP
   127    id: ldap
   128    config:
   129      host: localhost:389
   130      insecureNoSSL: true
   131      bindDN: cn=admin,dc=example,dc=org
   132      bindPW: admin
   133      usernamePrompt: Email Address
   134      userSearch:
   135        baseDN: ou=People,dc=example,dc=org
   136        filter: "(objectClass=person)"
   137        username: mail
   138        idAttr: DN
   139        emailAttr: mail
   140        nameAttr: cn
   141      groupSearch:
   142        baseDN: ou=Groups,dc=example,dc=org
   143        filter: "(objectClass=groupOfNames)"
   144        nameAttr: cn
   145  `
   146  
   147  var goodDexConfigWithLogger = `
   148  logger:
   149    level: debug
   150    other: value
   151  connectors:
   152  # GitHub example
   153  - type: github
   154    id: github
   155    name: GitHub
   156    config:
   157      clientID: aabbccddeeff00112233
   158      clientSecret: $dex.github.clientSecret
   159      orgs:
   160      - name: your-github-org
   161  
   162  # GitHub enterprise example
   163  - type: github
   164    id: acme-github
   165    name: Acme GitHub
   166    config:
   167      hostName: github.acme.example.com
   168      clientID: abcdefghijklmnopqrst
   169      clientSecret: $dex.acme.clientSecret
   170      orgs:
   171      - name: your-github-org
   172  `
   173  
   174  var goodSecrets = map[string]string{
   175  	"dex.github.clientSecret": "foobar",
   176  	"dex.acme.clientSecret":   "barfoo",
   177  }
   178  
   179  var goodSecretswithCRLF = map[string]string{
   180  	"dex.github.clientSecret": "foobar\n\r",
   181  	"dex.acme.clientSecret":   "barfoo\n\r",
   182  }
   183  
   184  func Test_GenerateDexConfig(t *testing.T) {
   185  	t.Run("Empty settings", func(t *testing.T) {
   186  		s := settings.ArgoCDSettings{}
   187  		config, err := GenerateDexConfigYAML(&s, false)
   188  		require.NoError(t, err)
   189  		assert.Nil(t, config)
   190  	})
   191  
   192  	t.Run("Invalid URL", func(t *testing.T) {
   193  		s := settings.ArgoCDSettings{
   194  			URL:       invalidURL,
   195  			DexConfig: goodDexConfig,
   196  		}
   197  		config, err := GenerateDexConfigYAML(&s, false)
   198  		require.Error(t, err)
   199  		assert.Nil(t, config)
   200  	})
   201  
   202  	t.Run("No URL set", func(t *testing.T) {
   203  		s := settings.ArgoCDSettings{
   204  			URL:       "",
   205  			DexConfig: "invalidyaml",
   206  		}
   207  		config, err := GenerateDexConfigYAML(&s, false)
   208  		require.NoError(t, err)
   209  		assert.Nil(t, config)
   210  	})
   211  
   212  	t.Run("Invalid YAML", func(t *testing.T) {
   213  		s := settings.ArgoCDSettings{
   214  			URL:       "http://localhost",
   215  			DexConfig: "invalidyaml",
   216  		}
   217  		config, err := GenerateDexConfigYAML(&s, false)
   218  		require.NoError(t, err)
   219  		assert.Nil(t, config)
   220  	})
   221  
   222  	t.Run("Valid YAML but incorrect Dex config", func(t *testing.T) {
   223  		s := settings.ArgoCDSettings{
   224  			URL:       "http://localhost",
   225  			DexConfig: malformedDexConfig,
   226  		}
   227  		config, err := GenerateDexConfigYAML(&s, false)
   228  		require.Error(t, err)
   229  		assert.Nil(t, config)
   230  	})
   231  
   232  	t.Run("Valid YAML but incorrect Dex config", func(t *testing.T) {
   233  		s := settings.ArgoCDSettings{
   234  			URL:       "http://localhost",
   235  			DexConfig: badDexConfig,
   236  		}
   237  		config, err := GenerateDexConfigYAML(&s, false)
   238  		require.Error(t, err)
   239  		assert.Nil(t, config)
   240  	})
   241  
   242  	t.Run("Valid YAML and correct Dex config", func(t *testing.T) {
   243  		s := settings.ArgoCDSettings{
   244  			URL:       "http://localhost",
   245  			DexConfig: goodDexConfig,
   246  		}
   247  		config, err := GenerateDexConfigYAML(&s, false)
   248  		require.NoError(t, err)
   249  		assert.NotNil(t, config)
   250  	})
   251  
   252  	t.Run("Secret dereference", func(t *testing.T) {
   253  		s := settings.ArgoCDSettings{
   254  			URL:       "http://localhost",
   255  			DexConfig: goodDexConfig,
   256  			Secrets:   goodSecrets,
   257  		}
   258  		config, err := GenerateDexConfigYAML(&s, false)
   259  		require.NoError(t, err)
   260  		assert.NotNil(t, config)
   261  		var dexCfg map[string]any
   262  		err = yaml.Unmarshal(config, &dexCfg)
   263  		if err != nil {
   264  			panic(err.Error())
   265  		}
   266  		connectors, ok := dexCfg["connectors"].([]any)
   267  		assert.True(t, ok)
   268  		for i, connectorsIf := range connectors {
   269  			config := connectorsIf.(map[string]any)["config"].(map[string]any)
   270  			switch i {
   271  			case 0:
   272  				assert.Equal(t, "foobar", config["clientSecret"])
   273  			case 1:
   274  				assert.Equal(t, "barfoo", config["clientSecret"])
   275  			}
   276  		}
   277  	})
   278  
   279  	t.Run("Secret dereference with extra white space", func(t *testing.T) {
   280  		s := settings.ArgoCDSettings{
   281  			URL:       "http://localhost",
   282  			DexConfig: goodDexConfig,
   283  			Secrets:   goodSecretswithCRLF,
   284  		}
   285  		config, err := GenerateDexConfigYAML(&s, false)
   286  		require.NoError(t, err)
   287  		assert.NotNil(t, config)
   288  		var dexCfg map[string]any
   289  		err = yaml.Unmarshal(config, &dexCfg)
   290  		if err != nil {
   291  			panic(err.Error())
   292  		}
   293  		connectors, ok := dexCfg["connectors"].([]any)
   294  		assert.True(t, ok)
   295  		for i, connectorsIf := range connectors {
   296  			config := connectorsIf.(map[string]any)["config"].(map[string]any)
   297  			switch i {
   298  			case 0:
   299  				assert.Equal(t, "foobar", config["clientSecret"])
   300  			case 1:
   301  				assert.Equal(t, "barfoo", config["clientSecret"])
   302  			}
   303  		}
   304  	})
   305  
   306  	t.Run("Logging level", func(t *testing.T) {
   307  		s := settings.ArgoCDSettings{
   308  			URL:       "http://localhost",
   309  			DexConfig: goodDexConfig,
   310  		}
   311  		t.Setenv(common.EnvLogLevel, log.WarnLevel.String())
   312  		t.Setenv(common.EnvLogFormat, utillog.JsonFormat)
   313  
   314  		config, err := GenerateDexConfigYAML(&s, false)
   315  		require.NoError(t, err)
   316  		assert.NotNil(t, config)
   317  		var dexCfg map[string]any
   318  		err = yaml.Unmarshal(config, &dexCfg)
   319  		if err != nil {
   320  			panic(err.Error())
   321  		}
   322  		loggerCfg, ok := dexCfg["logger"].(map[string]any)
   323  		assert.True(t, ok)
   324  
   325  		level, ok := loggerCfg["level"].(string)
   326  		assert.True(t, ok)
   327  		assert.Equal(t, "WARN", level)
   328  
   329  		format, ok := loggerCfg["format"].(string)
   330  		assert.True(t, ok)
   331  		assert.Equal(t, "json", format)
   332  	})
   333  
   334  	t.Run("Logging level with config", func(t *testing.T) {
   335  		s := settings.ArgoCDSettings{
   336  			URL:       "http://localhost",
   337  			DexConfig: goodDexConfigWithLogger,
   338  		}
   339  		t.Setenv(common.EnvLogLevel, log.WarnLevel.String())
   340  		t.Setenv(common.EnvLogFormat, utillog.JsonFormat)
   341  
   342  		config, err := GenerateDexConfigYAML(&s, false)
   343  		require.NoError(t, err)
   344  		assert.NotNil(t, config)
   345  		var dexCfg map[string]any
   346  		err = yaml.Unmarshal(config, &dexCfg)
   347  		if err != nil {
   348  			panic(err.Error())
   349  		}
   350  		loggerCfg, ok := dexCfg["logger"].(map[string]any)
   351  		assert.True(t, ok)
   352  
   353  		level, ok := loggerCfg["level"].(string)
   354  		assert.True(t, ok)
   355  		assert.Equal(t, "debug", level)
   356  
   357  		format, ok := loggerCfg["format"].(string)
   358  		assert.True(t, ok)
   359  		assert.Equal(t, "json", format)
   360  
   361  		_, ok = loggerCfg["other"].(string)
   362  		assert.True(t, ok)
   363  	})
   364  
   365  	t.Run("Redirect config", func(t *testing.T) {
   366  		types := []string{"oidc", "saml", "microsoft", "linkedin", "gitlab", "github", "bitbucket-cloud", "openshift", "gitea", "google", "oauth"}
   367  		for _, c := range types {
   368  			assert.True(t, needsRedirectURI(c))
   369  		}
   370  		assert.False(t, needsRedirectURI("invalid"))
   371  	})
   372  
   373  	t.Run("Custom static clients", func(t *testing.T) {
   374  		s := settings.ArgoCDSettings{
   375  			URL:       "http://localhost",
   376  			DexConfig: customStaticClientDexConfig,
   377  			Secrets:   goodSecretswithCRLF,
   378  		}
   379  		config, err := GenerateDexConfigYAML(&s, false)
   380  		require.NoError(t, err)
   381  		assert.NotNil(t, config)
   382  		var dexCfg map[string]any
   383  		err = yaml.Unmarshal(config, &dexCfg)
   384  		if err != nil {
   385  			panic(err.Error())
   386  		}
   387  		clients, ok := dexCfg["staticClients"].([]any)
   388  		assert.True(t, ok)
   389  		assert.Len(t, clients, 4)
   390  
   391  		customClient := clients[3].(map[string]any)
   392  		assert.Equal(t, "argo-workflow", customClient["id"].(string))
   393  		assert.Len(t, customClient["redirectURIs"].([]any), 1)
   394  	})
   395  	t.Run("Custom static clients secret dereference with trailing CRLF", func(t *testing.T) {
   396  		s := settings.ArgoCDSettings{
   397  			URL:       "http://localhost",
   398  			DexConfig: customStaticClientDexConfig,
   399  			Secrets:   goodSecretswithCRLF,
   400  		}
   401  		config, err := GenerateDexConfigYAML(&s, false)
   402  		require.NoError(t, err)
   403  		assert.NotNil(t, config)
   404  		var dexCfg map[string]any
   405  		err = yaml.Unmarshal(config, &dexCfg)
   406  		if err != nil {
   407  			panic(err.Error())
   408  		}
   409  		clients, ok := dexCfg["staticClients"].([]any)
   410  		assert.True(t, ok)
   411  		assert.Len(t, clients, 4)
   412  
   413  		customClient := clients[3].(map[string]any)
   414  		assert.Equal(t, "barfoo", customClient["secret"])
   415  	})
   416  	t.Run("Override dex oauth2 configuration", func(t *testing.T) {
   417  		s := settings.ArgoCDSettings{
   418  			URL:       "http://localhost",
   419  			DexConfig: goodDexConfigWithOauthOverrides,
   420  		}
   421  		config, err := GenerateDexConfigYAML(&s, false)
   422  		require.NoError(t, err)
   423  		assert.NotNil(t, config)
   424  		var dexCfg map[string]any
   425  		err = yaml.Unmarshal(config, &dexCfg)
   426  		if err != nil {
   427  			panic(err.Error())
   428  		}
   429  		oauth2Config, ok := dexCfg["oauth2"].(map[string]any)
   430  		assert.True(t, ok)
   431  		pwConn, ok := oauth2Config["passwordConnector"].(string)
   432  		assert.True(t, ok)
   433  		assert.Equal(t, "ldap", pwConn)
   434  
   435  		skipApprScr, ok := oauth2Config["skipApprovalScreen"].(bool)
   436  		assert.True(t, ok)
   437  		assert.True(t, skipApprScr)
   438  	})
   439  	t.Run("Override dex oauth2 with enabled ApprovalScreen", func(t *testing.T) {
   440  		s := settings.ArgoCDSettings{
   441  			URL:       "http://localhost",
   442  			DexConfig: goodDexConfigWithEnabledApprovalScreen,
   443  		}
   444  		config, err := GenerateDexConfigYAML(&s, false)
   445  		require.NoError(t, err)
   446  		assert.NotNil(t, config)
   447  		var dexCfg map[string]any
   448  		err = yaml.Unmarshal(config, &dexCfg)
   449  		if err != nil {
   450  			panic(err.Error())
   451  		}
   452  		oauth2Config, ok := dexCfg["oauth2"].(map[string]any)
   453  		assert.True(t, ok)
   454  		pwConn, ok := oauth2Config["passwordConnector"].(string)
   455  		assert.True(t, ok)
   456  		assert.Equal(t, "ldap", pwConn)
   457  
   458  		skipApprScr, ok := oauth2Config["skipApprovalScreen"].(bool)
   459  		assert.True(t, ok)
   460  		assert.False(t, skipApprScr)
   461  	})
   462  }
   463  
   464  func Test_DexReverseProxy(t *testing.T) {
   465  	t.Run("Good case", func(t *testing.T) {
   466  		var host string
   467  		fakeDex := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
   468  			host = req.Host
   469  			rw.WriteHeader(http.StatusOK)
   470  		}))
   471  		defer fakeDex.Close()
   472  		fmt.Printf("Fake Dex listening on %s\n", fakeDex.URL)
   473  		server := httptest.NewServer(http.HandlerFunc(NewDexHTTPReverseProxy(fakeDex.URL, "/", nil)))
   474  		fmt.Printf("Fake API Server listening on %s\n", server.URL)
   475  		defer server.Close()
   476  		target, _ := url.Parse(fakeDex.URL)
   477  		resp, err := http.Get(server.URL)
   478  		assert.NotNil(t, resp)
   479  		require.NoError(t, err)
   480  		assert.Equal(t, http.StatusOK, resp.StatusCode)
   481  		assert.Equal(t, host, target.Host)
   482  		fmt.Printf("%s\n", resp.Status)
   483  	})
   484  
   485  	t.Run("Bad case", func(t *testing.T) {
   486  		fakeDex := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) {
   487  			rw.WriteHeader(http.StatusInternalServerError)
   488  		}))
   489  		defer fakeDex.Close()
   490  		fmt.Printf("Fake Dex listening on %s\n", fakeDex.URL)
   491  		server := httptest.NewServer(http.HandlerFunc(NewDexHTTPReverseProxy(fakeDex.URL, "/", nil)))
   492  		fmt.Printf("Fake API Server listening on %s\n", server.URL)
   493  		defer server.Close()
   494  		client := &http.Client{
   495  			CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
   496  				return http.ErrUseLastResponse
   497  			},
   498  		}
   499  		resp, err := client.Get(server.URL)
   500  		assert.NotNil(t, resp)
   501  		require.NoError(t, err)
   502  		assert.Equal(t, http.StatusSeeOther, resp.StatusCode)
   503  		location, _ := resp.Location()
   504  		fmt.Printf("%s %s\n", resp.Status, location.RequestURI())
   505  		assert.True(t, strings.HasPrefix(location.RequestURI(), "/login?has_sso_error=true"))
   506  	})
   507  
   508  	t.Run("Invalid URL for Dex reverse proxy", func(t *testing.T) {
   509  		// Can't test for now, since it would call exit
   510  		t.Skip()
   511  		f := NewDexHTTPReverseProxy(invalidURL, "/", nil)
   512  		assert.Nil(t, f)
   513  	})
   514  
   515  	t.Run("Round Tripper", func(t *testing.T) {
   516  		server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
   517  			assert.Equal(t, "/", req.URL.String())
   518  		}))
   519  		defer server.Close()
   520  		rt := NewDexRewriteURLRoundTripper(server.URL, http.DefaultTransport)
   521  		assert.NotNil(t, rt)
   522  		req, err := http.NewRequest(http.MethodGet, "/", bytes.NewBuffer([]byte("")))
   523  		require.NoError(t, err)
   524  		_, err = rt.RoundTrip(req)
   525  		require.NoError(t, err)
   526  		target, _ := url.Parse(server.URL)
   527  		assert.Equal(t, req.Host, target.Host)
   528  	})
   529  }