github.com/SUSE/skuba@v1.4.17/pkg/skuba/actions/auth/login_test.go (about)

     1  /*
     2   * Copyright (c) 2019 SUSE LLC.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   */
    17  
    18  package auth
    19  
    20  import (
    21  	"bytes"
    22  	"crypto/tls"
    23  	"fmt"
    24  	"log"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"reflect"
    31  	"strings"
    32  	"testing"
    33  
    34  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    35  )
    36  
    37  func startServer() *httptest.Server {
    38  	mux := http.NewServeMux()
    39  	mux.HandleFunc("/.well-known/openid-configuration", openIDHandler())
    40  	mux.HandleFunc("/auth", authSingleConnectorHandler())
    41  	mux.HandleFunc("/auth/local", authLocalHandler())
    42  	mux.HandleFunc("/auth/ldap", authLocalHandler())
    43  	mux.HandleFunc("/token", tokenHandler())
    44  	mux.HandleFunc("/approval", approvalHandler())
    45  
    46  	srv := httptest.NewUnstartedServer(mux)
    47  	cert, _ := tls.LoadX509KeyPair("testdata/localhost.crt", "testdata/localhost.key")
    48  	srv.TLS = &tls.Config{
    49  		Certificates: []tls.Certificate{cert},
    50  	}
    51  	srv.StartTLS()
    52  	return srv
    53  }
    54  
    55  func Test_Login(t *testing.T) {
    56  	tests := []struct {
    57  		name               string
    58  		srvCb              func() *httptest.Server
    59  		cfg                LoginConfig
    60  		expectedKubeConfCb func(string, string) *clientcmdapi.Config
    61  		expectedErrorMsg   string
    62  	}{
    63  		{
    64  			name:  "secure ssl/tls",
    65  			srvCb: startServer,
    66  			cfg: LoginConfig{
    67  				Username:    mockDefaultUsername,
    68  				Password:    mockDefaultPassword,
    69  				RootCAPath:  "testdata/localhost.crt",
    70  				ClusterName: "test-cluster-name",
    71  			},
    72  			expectedKubeConfCb: func(dexServerURL string, clusterName string) *clientcmdapi.Config {
    73  				url, _ := url.Parse(dexServerURL)
    74  
    75  				kubeConfig := clientcmdapi.NewConfig()
    76  				kubeConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
    77  					Server:                   fmt.Sprintf("%s://%s:%s", defaultScheme, url.Hostname(), defaultAPIServerPort),
    78  					CertificateAuthorityData: localhostCert,
    79  				}
    80  				kubeConfig.Contexts[clusterName] = &clientcmdapi.Context{
    81  					Cluster:  clusterName,
    82  					AuthInfo: mockDefaultUsername,
    83  				}
    84  				kubeConfig.CurrentContext = clusterName
    85  				kubeConfig.AuthInfos[mockDefaultUsername] = &clientcmdapi.AuthInfo{
    86  					AuthProvider: &clientcmdapi.AuthProviderConfig{
    87  						Name: authProviderID,
    88  						Config: map[string]string{
    89  							"idp-issuer-url": dexServerURL,
    90  							"client-id":      clientID,
    91  							"client-secret":  clientSecret,
    92  							"id-token":       mockIDToken,
    93  							"refresh-token":  mockRefreshToken,
    94  						},
    95  					},
    96  				}
    97  				return kubeConfig
    98  			},
    99  		},
   100  		{
   101  			name:  "insecure ssl/tls",
   102  			srvCb: startServer,
   103  			cfg: LoginConfig{
   104  				Username:           mockDefaultUsername,
   105  				Password:           mockDefaultPassword,
   106  				InsecureSkipVerify: true,
   107  				ClusterName:        "test-cluster-name",
   108  			},
   109  			expectedKubeConfCb: func(dexServerURL string, clusterName string) *clientcmdapi.Config {
   110  				url, _ := url.Parse(dexServerURL)
   111  
   112  				kubeConfig := clientcmdapi.NewConfig()
   113  				kubeConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
   114  					Server:                fmt.Sprintf("%s://%s:%s", defaultScheme, url.Hostname(), defaultAPIServerPort),
   115  					InsecureSkipTLSVerify: true,
   116  				}
   117  				kubeConfig.Contexts[clusterName] = &clientcmdapi.Context{
   118  					Cluster:  clusterName,
   119  					AuthInfo: mockDefaultUsername,
   120  				}
   121  				kubeConfig.CurrentContext = clusterName
   122  				kubeConfig.AuthInfos[mockDefaultUsername] = &clientcmdapi.AuthInfo{
   123  					AuthProvider: &clientcmdapi.AuthProviderConfig{
   124  						Name: authProviderID,
   125  						Config: map[string]string{
   126  							"idp-issuer-url": dexServerURL,
   127  							"client-id":      clientID,
   128  							"client-secret":  clientSecret,
   129  							"id-token":       mockIDToken,
   130  							"refresh-token":  mockRefreshToken,
   131  						},
   132  					},
   133  				}
   134  				return kubeConfig
   135  			},
   136  		},
   137  		{
   138  			name: "multiple connectors",
   139  			srvCb: func() *httptest.Server {
   140  				mux := http.NewServeMux()
   141  				mux.HandleFunc("/.well-known/openid-configuration", openIDHandler())
   142  				mux.HandleFunc("/auth", authMultipleConnectorsHandler())
   143  				mux.HandleFunc("/auth/local", authLocalHandler())
   144  				mux.HandleFunc("/auth/ldap", authLocalHandler())
   145  				mux.HandleFunc("/token", tokenHandler())
   146  				mux.HandleFunc("/approval", approvalHandler())
   147  				return httptest.NewTLSServer(mux)
   148  			},
   149  			cfg: LoginConfig{
   150  				Username:           mockDefaultUsername,
   151  				Password:           mockDefaultPassword,
   152  				ClusterName:        "test-cluster-name",
   153  				InsecureSkipVerify: true,
   154  				AuthConnector:      "ldap",
   155  			},
   156  			expectedKubeConfCb: func(dexServerURL string, clusterName string) *clientcmdapi.Config {
   157  				url, _ := url.Parse(dexServerURL)
   158  
   159  				kubeConfig := clientcmdapi.NewConfig()
   160  				kubeConfig.Clusters[clusterName] = &clientcmdapi.Cluster{
   161  					Server:                fmt.Sprintf("%s://%s:%s", defaultScheme, url.Hostname(), defaultAPIServerPort),
   162  					InsecureSkipTLSVerify: true,
   163  				}
   164  				kubeConfig.Contexts[clusterName] = &clientcmdapi.Context{
   165  					Cluster:  clusterName,
   166  					AuthInfo: mockDefaultUsername,
   167  				}
   168  				kubeConfig.CurrentContext = clusterName
   169  				kubeConfig.AuthInfos[mockDefaultUsername] = &clientcmdapi.AuthInfo{
   170  					AuthProvider: &clientcmdapi.AuthProviderConfig{
   171  						Name: authProviderID,
   172  						Config: map[string]string{
   173  							"idp-issuer-url": dexServerURL,
   174  							"client-id":      clientID,
   175  							"client-secret":  clientSecret,
   176  							"id-token":       mockIDToken,
   177  							"refresh-token":  mockRefreshToken,
   178  						},
   179  					},
   180  				}
   181  				return kubeConfig
   182  			},
   183  		},
   184  		{
   185  			name: "no matched connector",
   186  			srvCb: func() *httptest.Server {
   187  				mux := http.NewServeMux()
   188  				mux.HandleFunc("/.well-known/openid-configuration", openIDHandler())
   189  				mux.HandleFunc("/auth", authMultipleConnectorsHandler())
   190  				return httptest.NewTLSServer(mux)
   191  			},
   192  			cfg: LoginConfig{
   193  				Username:           mockDefaultUsername,
   194  				Password:           mockDefaultPassword,
   195  				ClusterName:        "test-cluster-name",
   196  				InsecureSkipVerify: true,
   197  				AuthConnector:      "ldap123",
   198  			},
   199  			expectedErrorMsg: "auth failed: invalid input auth connector ID",
   200  		},
   201  		{
   202  			name:  "invalid root ca",
   203  			srvCb: startServer,
   204  			cfg: LoginConfig{
   205  				Username:    mockDefaultUsername,
   206  				Password:    mockDefaultPassword,
   207  				RootCAPath:  "testdata/invalid.crt",
   208  				ClusterName: "test-cluster-name",
   209  			},
   210  			expectedErrorMsg: "auth failed: no valid certificates found in root CA file",
   211  		},
   212  		{
   213  			name:  "cert file not exist",
   214  			srvCb: startServer,
   215  			cfg: LoginConfig{
   216  				Username:    mockDefaultUsername,
   217  				Password:    mockDefaultPassword,
   218  				RootCAPath:  "testdata/nonexist.crt",
   219  				ClusterName: "test-cluster-name",
   220  			},
   221  			expectedErrorMsg: "read CA failed: open testdata/nonexist.crt: no such file or directory",
   222  		},
   223  		{
   224  			name:  "auth failed",
   225  			srvCb: startServer,
   226  			cfg: LoginConfig{
   227  				Username:           "admin",
   228  				Password:           "1234",
   229  				InsecureSkipVerify: true,
   230  			},
   231  			expectedErrorMsg: "auth failed: invalid username or password",
   232  		},
   233  		{
   234  			name:  "invalid url",
   235  			srvCb: startServer,
   236  			cfg: LoginConfig{
   237  				DexServer:          "http://%41:8080/",
   238  				Username:           mockDefaultUsername,
   239  				Password:           mockDefaultPassword,
   240  				InsecureSkipVerify: true,
   241  			},
   242  			expectedErrorMsg: "parse url: parse http://%41:8080/: invalid URL escape \"%41\"",
   243  		},
   244  		{
   245  			name:  "oidc server with incorrect port number",
   246  			srvCb: startServer,
   247  			cfg: LoginConfig{
   248  				DexServer:   "http://127.0.0.1:32001/",
   249  				Username:    mockDefaultUsername,
   250  				Password:    mockDefaultPassword,
   251  				ClusterName: "test-cluster-name",
   252  			},
   253  			expectedErrorMsg: fmt.Sprintf("auth failed: failed to query provider http://127.0.0.1:32001/ (is this the right URL? maybe missing --root-ca or --insecure, or incorrect port number?)"),
   254  		},
   255  		{
   256  			name: "issuer scopes supported invalid",
   257  			srvCb: func() *httptest.Server {
   258  				mux := http.NewServeMux()
   259  				mux.HandleFunc("/.well-known/openid-configuration", openIDHandlerInvalidScopes())
   260  				return httptest.NewTLSServer(mux)
   261  			},
   262  			cfg: LoginConfig{
   263  				Username:           mockDefaultUsername,
   264  				Password:           mockDefaultPassword,
   265  				InsecureSkipVerify: true,
   266  				ClusterName:        "test-cluster-name",
   267  			},
   268  			expectedErrorMsg: "auth failed: failed to parse provider scopes_supported: json: cannot unmarshal number into Go struct field .scopes_supported of type string",
   269  		},
   270  		{
   271  			name: "issuer no claims",
   272  			srvCb: func() *httptest.Server {
   273  				mux := http.NewServeMux()
   274  				mux.HandleFunc("/.well-known/openid-configuration", openIDHandlerNoScopes())
   275  				return httptest.NewTLSServer(mux)
   276  			},
   277  			cfg: LoginConfig{
   278  				Username:           mockDefaultUsername,
   279  				Password:           mockDefaultPassword,
   280  				InsecureSkipVerify: true,
   281  				ClusterName:        "test-cluster-name",
   282  			},
   283  			expectedErrorMsg: "auth failed: failed on get auth code url: Get ?access_type=offline&client_id=oidc-cli&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=audience%3Aserver%3Aclient_id%3Aoidc: unsupported protocol scheme \"\"",
   284  		},
   285  		{
   286  			name: "approval body content incorrect",
   287  			srvCb: func() *httptest.Server {
   288  				mux := http.NewServeMux()
   289  				mux.HandleFunc("/.well-known/openid-configuration", openIDHandler())
   290  				mux.HandleFunc("/auth", authSingleConnectorHandler())
   291  				mux.HandleFunc("/auth/local", authLocalHandler())
   292  				mux.HandleFunc("/token", tokenHandler())
   293  				mux.HandleFunc("/approval", approvalInvalidBodyHandler())
   294  				return httptest.NewTLSServer(mux)
   295  			},
   296  			cfg: LoginConfig{
   297  				Username:           mockDefaultUsername,
   298  				Password:           mockDefaultPassword,
   299  				InsecureSkipVerify: true,
   300  				ClusterName:        "test-cluster-name",
   301  			},
   302  			expectedErrorMsg: "auth failed: failed to extract token from OOB response",
   303  		},
   304  	}
   305  
   306  	for _, tt := range tests {
   307  		tt := tt
   308  		t.Run(tt.name, func(t *testing.T) {
   309  			testSrv := tt.srvCb()
   310  			defer testSrv.Close()
   311  
   312  			if tt.cfg.DexServer == "" {
   313  				tt.cfg.DexServer = testSrv.URL
   314  			}
   315  			gotKubeConfig, err := Login(tt.cfg)
   316  
   317  			if tt.expectedErrorMsg != "" {
   318  				if err == nil {
   319  					t.Errorf("error expected on %s, but no error reported", tt.name)
   320  					return
   321  				}
   322  				if err.Error() != tt.expectedErrorMsg {
   323  					t.Errorf("got error msg %s, want %s", err.Error(), tt.expectedErrorMsg)
   324  					return
   325  				}
   326  				return
   327  			}
   328  
   329  			if err != nil {
   330  				t.Errorf("error not expected on %s, but an error was reported (%v)", tt.name, err)
   331  				return
   332  			}
   333  
   334  			expectKubeConfig := tt.expectedKubeConfCb(tt.cfg.DexServer, tt.cfg.ClusterName)
   335  			if !reflect.DeepEqual(gotKubeConfig, expectKubeConfig) {
   336  				t.Errorf("got %v, want %v", gotKubeConfig, expectKubeConfig)
   337  				return
   338  			}
   339  		})
   340  	}
   341  }
   342  
   343  func Test_LoginDebug(t *testing.T) {
   344  	testServer := startServer()
   345  	defer testServer.Close()
   346  
   347  	// capture log output
   348  	var buf bytes.Buffer
   349  	log.SetOutput(&buf)
   350  
   351  	_, err := doAuth(request{
   352  		clientID:           clientID,
   353  		clientSecret:       clientSecret,
   354  		IssuerURL:          testServer.URL,
   355  		Username:           mockDefaultUsername,
   356  		Password:           mockDefaultPassword,
   357  		InsecureSkipVerify: true,
   358  		Debug:              true,
   359  	})
   360  	if err != nil {
   361  		t.Errorf("error not expected, but an error was reported (%v)", err)
   362  		return
   363  	}
   364  
   365  	if strings.Contains(buf.String(), mockDefaultPassword) {
   366  		t.Error("password is not REDACTED")
   367  	}
   368  	if !strings.Contains(buf.String(), "REDACTED") {
   369  		t.Error("password is not change to REDACTED")
   370  	}
   371  }
   372  
   373  func Test_SaveKubeconfig(t *testing.T) {
   374  	tests := []struct {
   375  		name          string
   376  		filename      string
   377  		kubeConfig    *clientcmdapi.Config
   378  		expectedError bool
   379  	}{
   380  		{
   381  			name:       "success output",
   382  			kubeConfig: clientcmdapi.NewConfig(),
   383  		},
   384  		{
   385  			name:          "open file failed",
   386  			filename:      "path/to/kubeconfig",
   387  			kubeConfig:    clientcmdapi.NewConfig(),
   388  			expectedError: true,
   389  		},
   390  		{
   391  			name:          "encode failed",
   392  			kubeConfig:    nil,
   393  			expectedError: true,
   394  		},
   395  	}
   396  
   397  	for _, tt := range tests {
   398  		tt := tt
   399  		t.Run(tt.name, func(t *testing.T) {
   400  			var path string
   401  			if tt.filename != "" {
   402  				path = filepath.Join("testdata", tt.filename+".golden")
   403  			} else {
   404  				path = filepath.Join("testdata", tt.name+".golden")
   405  			}
   406  			err := SaveKubeconfig(path, tt.kubeConfig)
   407  			defer os.Remove(path)
   408  
   409  			if tt.expectedError && err == nil {
   410  				t.Errorf("error expected on %s, but no error reported", tt.name)
   411  				return
   412  			} else if !tt.expectedError && err != nil {
   413  				t.Errorf("error not expected on %s, but an error was reported (%v)", tt.name, err)
   414  				return
   415  			}
   416  		})
   417  	}
   418  }