1package login 2 3import ( 4 "io/ioutil" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/hashicorp/consul/agent" 12 "github.com/hashicorp/consul/agent/consul/authmethod/kubeauth" 13 "github.com/hashicorp/consul/agent/consul/authmethod/testauth" 14 "github.com/hashicorp/consul/api" 15 "github.com/hashicorp/consul/command/acl" 16 "github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest" 17 "github.com/hashicorp/consul/sdk/freeport" 18 "github.com/hashicorp/consul/sdk/testutil" 19 "github.com/hashicorp/consul/testrpc" 20 "github.com/mitchellh/cli" 21 "github.com/stretchr/testify/require" 22 "gopkg.in/square/go-jose.v2/jwt" 23) 24 25func TestLoginCommand_noTabs(t *testing.T) { 26 t.Parallel() 27 28 if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') { 29 t.Fatal("help has tabs") 30 } 31} 32 33func TestLoginCommand(t *testing.T) { 34 if testing.Short() { 35 t.Skip("too slow for testing.Short") 36 } 37 38 t.Parallel() 39 40 testDir := testutil.TempDir(t, "acl") 41 42 a := agent.NewTestAgent(t, ` 43 primary_datacenter = "dc1" 44 acl { 45 enabled = true 46 tokens { 47 master = "root" 48 } 49 }`) 50 51 defer a.Shutdown() 52 testrpc.WaitForLeader(t, a.RPC, "dc1") 53 54 client := a.Client() 55 56 t.Run("method is required", func(t *testing.T) { 57 ui := cli.NewMockUi() 58 cmd := New(ui) 59 60 args := []string{ 61 "-http-addr=" + a.HTTPAddr(), 62 "-token=root", 63 } 64 65 code := cmd.Run(args) 66 require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String()) 67 require.Contains(t, ui.ErrorWriter.String(), "Missing required '-method' flag") 68 }) 69 70 tokenSinkFile := filepath.Join(testDir, "test.token") 71 72 t.Run("token-sink-file is required", func(t *testing.T) { 73 ui := cli.NewMockUi() 74 cmd := New(ui) 75 76 args := []string{ 77 "-http-addr=" + a.HTTPAddr(), 78 "-token=root", 79 "-method=test", 80 } 81 82 code := cmd.Run(args) 83 require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String()) 84 require.Contains(t, ui.ErrorWriter.String(), "Missing required '-token-sink-file' flag") 85 }) 86 87 t.Run("bearer-token-file is required", func(t *testing.T) { 88 defer os.Remove(tokenSinkFile) 89 90 ui := cli.NewMockUi() 91 cmd := New(ui) 92 93 args := []string{ 94 "-http-addr=" + a.HTTPAddr(), 95 "-token=root", 96 "-method=test", 97 "-token-sink-file", tokenSinkFile, 98 } 99 100 code := cmd.Run(args) 101 require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String()) 102 require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bearer-token-file' flag") 103 }) 104 105 bearerTokenFile := filepath.Join(testDir, "bearer.token") 106 107 t.Run("bearer-token-file is empty", func(t *testing.T) { 108 defer os.Remove(tokenSinkFile) 109 110 require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(""), 0600)) 111 112 ui := cli.NewMockUi() 113 cmd := New(ui) 114 115 args := []string{ 116 "-http-addr=" + a.HTTPAddr(), 117 "-token=root", 118 "-method=test", 119 "-token-sink-file", tokenSinkFile, 120 "-bearer-token-file", bearerTokenFile, 121 } 122 123 code := cmd.Run(args) 124 require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String()) 125 require.Contains(t, ui.ErrorWriter.String(), "No bearer token found in") 126 }) 127 128 require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte("demo-token"), 0600)) 129 130 t.Run("try login with no method configured", func(t *testing.T) { 131 defer os.Remove(tokenSinkFile) 132 133 ui := cli.NewMockUi() 134 cmd := New(ui) 135 136 args := []string{ 137 "-http-addr=" + a.HTTPAddr(), 138 "-token=root", 139 "-method=test", 140 "-token-sink-file", tokenSinkFile, 141 "-bearer-token-file", bearerTokenFile, 142 } 143 144 code := cmd.Run(args) 145 require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String()) 146 require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found: auth method \"test\" not found") 147 }) 148 149 testSessionID := testauth.StartSession() 150 defer testauth.ResetSession(testSessionID) 151 152 testauth.InstallSessionToken( 153 testSessionID, 154 "demo-token", 155 "default", "demo", "76091af4-4b56-11e9-ac4b-708b11801cbe", 156 ) 157 158 { 159 _, _, err := client.ACL().AuthMethodCreate( 160 &api.ACLAuthMethod{ 161 Name: "test", 162 Type: "testing", 163 Config: map[string]interface{}{ 164 "SessionID": testSessionID, 165 }, 166 }, 167 &api.WriteOptions{Token: "root"}, 168 ) 169 require.NoError(t, err) 170 } 171 172 t.Run("try login with method configured but no binding rules", func(t *testing.T) { 173 defer os.Remove(tokenSinkFile) 174 175 ui := cli.NewMockUi() 176 cmd := New(ui) 177 178 args := []string{ 179 "-http-addr=" + a.HTTPAddr(), 180 "-token=root", 181 "-method=test", 182 "-token-sink-file", tokenSinkFile, 183 "-bearer-token-file", bearerTokenFile, 184 } 185 186 code := cmd.Run(args) 187 require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String()) 188 require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)") 189 }) 190 191 { 192 _, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{ 193 AuthMethod: "test", 194 BindType: api.BindingRuleBindTypeService, 195 BindName: "${serviceaccount.name}", 196 Selector: "serviceaccount.namespace==default", 197 }, 198 &api.WriteOptions{Token: "root"}, 199 ) 200 require.NoError(t, err) 201 } 202 203 t.Run("try login with method configured and binding rules", func(t *testing.T) { 204 defer os.Remove(tokenSinkFile) 205 206 ui := cli.NewMockUi() 207 cmd := New(ui) 208 209 args := []string{ 210 "-http-addr=" + a.HTTPAddr(), 211 "-token=root", 212 "-method=test", 213 "-token-sink-file", tokenSinkFile, 214 "-bearer-token-file", bearerTokenFile, 215 } 216 217 code := cmd.Run(args) 218 require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String()) 219 require.Empty(t, ui.ErrorWriter.String()) 220 require.Empty(t, ui.OutputWriter.String()) 221 222 raw, err := ioutil.ReadFile(tokenSinkFile) 223 require.NoError(t, err) 224 225 token := strings.TrimSpace(string(raw)) 226 require.Len(t, token, 36, "must be a valid uid: %s", token) 227 }) 228} 229 230func TestLoginCommand_k8s(t *testing.T) { 231 if testing.Short() { 232 t.Skip("too slow for testing.Short") 233 } 234 235 t.Parallel() 236 237 testDir := testutil.TempDir(t, "acl") 238 239 a := agent.NewTestAgent(t, ` 240 primary_datacenter = "dc1" 241 acl { 242 enabled = true 243 tokens { 244 master = "root" 245 } 246 }`) 247 248 defer a.Shutdown() 249 testrpc.WaitForLeader(t, a.RPC, "dc1") 250 251 client := a.Client() 252 253 tokenSinkFile := filepath.Join(testDir, "test.token") 254 bearerTokenFile := filepath.Join(testDir, "bearer.token") 255 256 // the "B" jwt will be the one being reviewed 257 require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(acl.TestKubernetesJWT_B), 0600)) 258 259 // spin up a fake api server 260 testSrv := kubeauth.StartTestAPIServer(t) 261 defer testSrv.Stop() 262 263 testSrv.AuthorizeJWT(acl.TestKubernetesJWT_A) 264 testSrv.SetAllowedServiceAccount( 265 "default", 266 "demo", 267 "76091af4-4b56-11e9-ac4b-708b11801cbe", 268 "", 269 acl.TestKubernetesJWT_B, 270 ) 271 272 { 273 _, _, err := client.ACL().AuthMethodCreate( 274 &api.ACLAuthMethod{ 275 Name: "k8s", 276 Type: "kubernetes", 277 Config: map[string]interface{}{ 278 "Host": testSrv.Addr(), 279 "CACert": testSrv.CACert(), 280 // the "A" jwt will be the one with token review privs 281 "ServiceAccountJWT": acl.TestKubernetesJWT_A, 282 }, 283 }, 284 &api.WriteOptions{Token: "root"}, 285 ) 286 require.NoError(t, err) 287 } 288 289 { 290 _, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{ 291 AuthMethod: "k8s", 292 BindType: api.BindingRuleBindTypeService, 293 BindName: "${serviceaccount.name}", 294 Selector: "serviceaccount.namespace==default", 295 }, 296 &api.WriteOptions{Token: "root"}, 297 ) 298 require.NoError(t, err) 299 } 300 301 t.Run("try login with method configured and binding rules", func(t *testing.T) { 302 defer os.Remove(tokenSinkFile) 303 304 ui := cli.NewMockUi() 305 cmd := New(ui) 306 307 args := []string{ 308 "-http-addr=" + a.HTTPAddr(), 309 "-token=root", 310 "-method=k8s", 311 "-token-sink-file", tokenSinkFile, 312 "-bearer-token-file", bearerTokenFile, 313 } 314 315 code := cmd.Run(args) 316 require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String()) 317 require.Empty(t, ui.ErrorWriter.String()) 318 require.Empty(t, ui.OutputWriter.String()) 319 320 raw, err := ioutil.ReadFile(tokenSinkFile) 321 require.NoError(t, err) 322 323 token := strings.TrimSpace(string(raw)) 324 require.Len(t, token, 36, "must be a valid uid: %s", token) 325 }) 326} 327 328func TestLoginCommand_jwt(t *testing.T) { 329 if testing.Short() { 330 t.Skip("too slow for testing.Short") 331 } 332 333 t.Parallel() 334 335 testDir := testutil.TempDir(t, "acl") 336 337 a := agent.NewTestAgent(t, ` 338 primary_datacenter = "dc1" 339 acl { 340 enabled = true 341 tokens { 342 master = "root" 343 } 344 }`) 345 346 defer a.Shutdown() 347 testrpc.WaitForLeader(t, a.RPC, "dc1") 348 349 client := a.Client() 350 351 tokenSinkFile := filepath.Join(testDir, "test.token") 352 bearerTokenFile := filepath.Join(testDir, "bearer.token") 353 354 // spin up a fake oidc server 355 oidcServer := startSSOTestServer(t) 356 pubKey, privKey := oidcServer.SigningKeys() 357 358 type mConfig = map[string]interface{} 359 cases := map[string]struct { 360 f func(config mConfig) 361 issuer string 362 expectErr string 363 }{ 364 "success - jwt static keys": {func(config mConfig) { 365 config["BoundIssuer"] = "https://legit.issuer.internal/" 366 config["JWTValidationPubKeys"] = []string{pubKey} 367 }, 368 "https://legit.issuer.internal/", 369 ""}, 370 "success - jwt jwks": {func(config mConfig) { 371 config["JWKSURL"] = oidcServer.Addr() + "/certs" 372 config["JWKSCACert"] = oidcServer.CACert() 373 }, 374 "https://legit.issuer.internal/", 375 ""}, 376 "success - jwt oidc discovery": {func(config mConfig) { 377 config["OIDCDiscoveryURL"] = oidcServer.Addr() 378 config["OIDCDiscoveryCACert"] = oidcServer.CACert() 379 }, 380 oidcServer.Addr(), 381 ""}, 382 } 383 384 for name, tc := range cases { 385 tc := tc 386 t.Run(name, func(t *testing.T) { 387 method := &api.ACLAuthMethod{ 388 Name: "jwt", 389 Type: "jwt", 390 Config: map[string]interface{}{ 391 "JWTSupportedAlgs": []string{"ES256"}, 392 "ClaimMappings": map[string]string{ 393 "first_name": "name", 394 "/org/primary": "primary_org", 395 }, 396 "ListClaimMappings": map[string]string{ 397 "https://consul.test/groups": "groups", 398 }, 399 "BoundAudiences": []string{"https://consul.test"}, 400 }, 401 } 402 if tc.f != nil { 403 tc.f(method.Config) 404 } 405 _, _, err := client.ACL().AuthMethodCreate( 406 method, 407 &api.WriteOptions{Token: "root"}, 408 ) 409 require.NoError(t, err) 410 411 _, _, err = client.ACL().BindingRuleCreate(&api.ACLBindingRule{ 412 AuthMethod: "jwt", 413 BindType: api.BindingRuleBindTypeService, 414 BindName: "test--${value.name}--${value.primary_org}", 415 Selector: "value.name == jeff2 and value.primary_org == engineering and foo in list.groups", 416 }, 417 &api.WriteOptions{Token: "root"}, 418 ) 419 require.NoError(t, err) 420 421 cl := jwt.Claims{ 422 Subject: "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", 423 Audience: jwt.Audience{"https://consul.test"}, 424 Issuer: tc.issuer, 425 NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)), 426 Expiry: jwt.NewNumericDate(time.Now().Add(5 * time.Second)), 427 } 428 429 type orgs struct { 430 Primary string `json:"primary"` 431 } 432 433 privateCl := struct { 434 FirstName string `json:"first_name"` 435 Org orgs `json:"org"` 436 Groups []string `json:"https://consul.test/groups"` 437 }{ 438 FirstName: "jeff2", 439 Org: orgs{"engineering"}, 440 Groups: []string{"foo", "bar"}, 441 } 442 443 // Drop a JWT on disk. 444 jwtData, err := oidcauthtest.SignJWT(privKey, cl, privateCl) 445 require.NoError(t, err) 446 require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(jwtData), 0600)) 447 448 defer os.Remove(tokenSinkFile) 449 ui := cli.NewMockUi() 450 cmd := New(ui) 451 452 args := []string{ 453 "-http-addr=" + a.HTTPAddr(), 454 "-token=root", 455 "-method=jwt", 456 "-token-sink-file", tokenSinkFile, 457 "-bearer-token-file", bearerTokenFile, 458 } 459 460 code := cmd.Run(args) 461 require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String()) 462 require.Empty(t, ui.ErrorWriter.String()) 463 require.Empty(t, ui.OutputWriter.String()) 464 465 raw, err := ioutil.ReadFile(tokenSinkFile) 466 require.NoError(t, err) 467 468 token := strings.TrimSpace(string(raw)) 469 require.Len(t, token, 36, "must be a valid uid: %s", token) 470 }) 471 } 472} 473 474func startSSOTestServer(t *testing.T) *oidcauthtest.Server { 475 ports := freeport.MustTake(1) 476 return oidcauthtest.Start(t, oidcauthtest.WithPort( 477 ports[0], 478 func() { freeport.Return(ports) }, 479 )) 480} 481