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 }