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