1// Copyright 2015 The etcd Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package v2auth 16 17import ( 18 "context" 19 "reflect" 20 "testing" 21 "time" 22 23 "go.etcd.io/etcd/etcdserver" 24 "go.etcd.io/etcd/etcdserver/api/v2error" 25 "go.etcd.io/etcd/etcdserver/api/v2store" 26 "go.etcd.io/etcd/etcdserver/etcdserverpb" 27 28 "go.uber.org/zap" 29) 30 31type fakeDoer struct{} 32 33func (fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) { 34 return etcdserver.Response{}, nil 35} 36 37func TestCheckPassword(t *testing.T) { 38 st := NewStore(zap.NewExample(), fakeDoer{}, 5*time.Second) 39 u := User{Password: "$2a$10$I3iddh1D..EIOXXQtsra4u8AjOtgEa2ERxVvYGfXFBJDo1omXwP.q"} 40 matched := st.CheckPassword(u, "foo") 41 if matched { 42 t.Fatalf("expected false, got %v", matched) 43 } 44} 45 46const testTimeout = time.Millisecond 47 48func TestMergeUser(t *testing.T) { 49 tbl := []struct { 50 input User 51 merge User 52 expect User 53 iserr bool 54 }{ 55 { 56 User{User: "foo"}, 57 User{User: "bar"}, 58 User{}, 59 true, 60 }, 61 { 62 User{User: "foo"}, 63 User{User: "foo"}, 64 User{User: "foo", Roles: []string{}}, 65 false, 66 }, 67 { 68 User{User: "foo"}, 69 User{User: "foo", Grant: []string{"role1"}}, 70 User{User: "foo", Roles: []string{"role1"}}, 71 false, 72 }, 73 { 74 User{User: "foo", Roles: []string{"role1"}}, 75 User{User: "foo", Grant: []string{"role1"}}, 76 User{}, 77 true, 78 }, 79 { 80 User{User: "foo", Roles: []string{"role1"}}, 81 User{User: "foo", Revoke: []string{"role2"}}, 82 User{}, 83 true, 84 }, 85 { 86 User{User: "foo", Roles: []string{"role1"}}, 87 User{User: "foo", Grant: []string{"role2"}}, 88 User{User: "foo", Roles: []string{"role1", "role2"}}, 89 false, 90 }, 91 { // empty password will not overwrite the previous password 92 User{User: "foo", Password: "foo", Roles: []string{}}, 93 User{User: "foo", Password: ""}, 94 User{User: "foo", Password: "foo", Roles: []string{}}, 95 false, 96 }, 97 } 98 99 for i, tt := range tbl { 100 out, err := tt.input.merge(zap.NewExample(), tt.merge, passwordStore{}) 101 if err != nil && !tt.iserr { 102 t.Fatalf("Got unexpected error on item %d", i) 103 } 104 if !tt.iserr { 105 if !reflect.DeepEqual(out, tt.expect) { 106 t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect) 107 } 108 } 109 } 110} 111 112func TestMergeRole(t *testing.T) { 113 tbl := []struct { 114 input Role 115 merge Role 116 expect Role 117 iserr bool 118 }{ 119 { 120 Role{Role: "foo"}, 121 Role{Role: "bar"}, 122 Role{}, 123 true, 124 }, 125 { 126 Role{Role: "foo"}, 127 Role{Role: "foo", Grant: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, 128 Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, 129 false, 130 }, 131 { 132 Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, 133 Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}}, 134 Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{}, Write: []string{}}}}, 135 false, 136 }, 137 { 138 Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/bardir"}}}}, 139 Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}}}}, 140 Role{}, 141 true, 142 }, 143 } 144 for i, tt := range tbl { 145 out, err := tt.input.merge(zap.NewExample(), tt.merge) 146 if err != nil && !tt.iserr { 147 t.Fatalf("Got unexpected error on item %d", i) 148 } 149 if !tt.iserr { 150 if !reflect.DeepEqual(out, tt.expect) { 151 t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect) 152 } 153 } 154 } 155} 156 157type testDoer struct { 158 get []etcdserver.Response 159 put []etcdserver.Response 160 getindex int 161 putindex int 162 explicitlyEnabled bool 163} 164 165func (td *testDoer) Do(_ context.Context, req etcdserverpb.Request) (etcdserver.Response, error) { 166 if td.explicitlyEnabled && (req.Path == StorePermsPrefix+"/enabled") { 167 t := "true" 168 return etcdserver.Response{ 169 Event: &v2store.Event{ 170 Action: v2store.Get, 171 Node: &v2store.NodeExtern{ 172 Key: StorePermsPrefix + "/users/cat", 173 Value: &t, 174 }, 175 }, 176 }, nil 177 } 178 if (req.Method == "GET" || req.Method == "QGET") && td.get != nil { 179 res := td.get[td.getindex] 180 if res.Event == nil { 181 td.getindex++ 182 return etcdserver.Response{}, &v2error.Error{ 183 ErrorCode: v2error.EcodeKeyNotFound, 184 } 185 } 186 td.getindex++ 187 return res, nil 188 } 189 if req.Method == "PUT" && td.put != nil { 190 res := td.put[td.putindex] 191 if res.Event == nil { 192 td.putindex++ 193 return etcdserver.Response{}, &v2error.Error{ 194 ErrorCode: v2error.EcodeNodeExist, 195 } 196 } 197 td.putindex++ 198 return res, nil 199 } 200 return etcdserver.Response{}, nil 201} 202 203func TestAllUsers(t *testing.T) { 204 d := &testDoer{ 205 get: []etcdserver.Response{ 206 { 207 Event: &v2store.Event{ 208 Action: v2store.Get, 209 Node: &v2store.NodeExtern{ 210 Nodes: v2store.NodeExterns([]*v2store.NodeExtern{ 211 { 212 Key: StorePermsPrefix + "/users/cat", 213 }, 214 { 215 Key: StorePermsPrefix + "/users/dog", 216 }, 217 }), 218 }, 219 }, 220 }, 221 }, 222 } 223 expected := []string{"cat", "dog"} 224 225 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} 226 users, err := s.AllUsers() 227 if err != nil { 228 t.Error("Unexpected error", err) 229 } 230 if !reflect.DeepEqual(users, expected) { 231 t.Error("AllUsers doesn't match given store. Got", users, "expected", expected) 232 } 233} 234 235func TestGetAndDeleteUser(t *testing.T) { 236 data := `{"user": "cat", "roles" : ["animal"]}` 237 d := &testDoer{ 238 get: []etcdserver.Response{ 239 { 240 Event: &v2store.Event{ 241 Action: v2store.Get, 242 Node: &v2store.NodeExtern{ 243 Key: StorePermsPrefix + "/users/cat", 244 Value: &data, 245 }, 246 }, 247 }, 248 }, 249 explicitlyEnabled: true, 250 } 251 expected := User{User: "cat", Roles: []string{"animal"}} 252 253 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} 254 out, err := s.GetUser("cat") 255 if err != nil { 256 t.Error("Unexpected error", err) 257 } 258 if !reflect.DeepEqual(out, expected) { 259 t.Error("GetUser doesn't match given store. Got", out, "expected", expected) 260 } 261 err = s.DeleteUser("cat") 262 if err != nil { 263 t.Error("Unexpected error", err) 264 } 265} 266 267func TestAllRoles(t *testing.T) { 268 d := &testDoer{ 269 get: []etcdserver.Response{ 270 { 271 Event: &v2store.Event{ 272 Action: v2store.Get, 273 Node: &v2store.NodeExtern{ 274 Nodes: v2store.NodeExterns([]*v2store.NodeExtern{ 275 { 276 Key: StorePermsPrefix + "/roles/animal", 277 }, 278 { 279 Key: StorePermsPrefix + "/roles/human", 280 }, 281 }), 282 }, 283 }, 284 }, 285 }, 286 explicitlyEnabled: true, 287 } 288 expected := []string{"animal", "human", "root"} 289 290 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} 291 out, err := s.AllRoles() 292 if err != nil { 293 t.Error("Unexpected error", err) 294 } 295 if !reflect.DeepEqual(out, expected) { 296 t.Error("AllRoles doesn't match given store. Got", out, "expected", expected) 297 } 298} 299 300func TestGetAndDeleteRole(t *testing.T) { 301 data := `{"role": "animal"}` 302 d := &testDoer{ 303 get: []etcdserver.Response{ 304 { 305 Event: &v2store.Event{ 306 Action: v2store.Get, 307 Node: &v2store.NodeExtern{ 308 Key: StorePermsPrefix + "/roles/animal", 309 Value: &data, 310 }, 311 }, 312 }, 313 }, 314 explicitlyEnabled: true, 315 } 316 expected := Role{Role: "animal"} 317 318 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} 319 out, err := s.GetRole("animal") 320 if err != nil { 321 t.Error("Unexpected error", err) 322 } 323 if !reflect.DeepEqual(out, expected) { 324 t.Error("GetRole doesn't match given store. Got", out, "expected", expected) 325 } 326 err = s.DeleteRole("animal") 327 if err != nil { 328 t.Error("Unexpected error", err) 329 } 330} 331 332func TestEnsure(t *testing.T) { 333 d := &testDoer{ 334 get: []etcdserver.Response{ 335 { 336 Event: &v2store.Event{ 337 Action: v2store.Set, 338 Node: &v2store.NodeExtern{ 339 Key: StorePermsPrefix, 340 Dir: true, 341 }, 342 }, 343 }, 344 { 345 Event: &v2store.Event{ 346 Action: v2store.Set, 347 Node: &v2store.NodeExtern{ 348 Key: StorePermsPrefix + "/users/", 349 Dir: true, 350 }, 351 }, 352 }, 353 { 354 Event: &v2store.Event{ 355 Action: v2store.Set, 356 Node: &v2store.NodeExtern{ 357 Key: StorePermsPrefix + "/roles/", 358 Dir: true, 359 }, 360 }, 361 }, 362 }, 363 } 364 365 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: false} 366 err := s.ensureAuthDirectories() 367 if err != nil { 368 t.Error("Unexpected error", err) 369 } 370} 371 372type fastPasswordStore struct { 373} 374 375func (fastPasswordStore) CheckPassword(user User, password string) bool { 376 return user.Password == password 377} 378 379func (fastPasswordStore) HashPassword(password string) (string, error) { return password, nil } 380 381func TestCreateAndUpdateUser(t *testing.T) { 382 olduser := `{"user": "cat", "roles" : ["animal"]}` 383 newuser := `{"user": "cat", "roles" : ["animal", "pet"]}` 384 d := &testDoer{ 385 get: []etcdserver.Response{ 386 { 387 Event: nil, 388 }, 389 { 390 Event: &v2store.Event{ 391 Action: v2store.Get, 392 Node: &v2store.NodeExtern{ 393 Key: StorePermsPrefix + "/users/cat", 394 Value: &olduser, 395 }, 396 }, 397 }, 398 { 399 Event: &v2store.Event{ 400 Action: v2store.Get, 401 Node: &v2store.NodeExtern{ 402 Key: StorePermsPrefix + "/users/cat", 403 Value: &olduser, 404 }, 405 }, 406 }, 407 }, 408 put: []etcdserver.Response{ 409 { 410 Event: &v2store.Event{ 411 Action: v2store.Update, 412 Node: &v2store.NodeExtern{ 413 Key: StorePermsPrefix + "/users/cat", 414 Value: &olduser, 415 }, 416 }, 417 }, 418 { 419 Event: &v2store.Event{ 420 Action: v2store.Update, 421 Node: &v2store.NodeExtern{ 422 Key: StorePermsPrefix + "/users/cat", 423 Value: &newuser, 424 }, 425 }, 426 }, 427 }, 428 explicitlyEnabled: true, 429 } 430 user := User{User: "cat", Password: "meow", Roles: []string{"animal"}} 431 update := User{User: "cat", Grant: []string{"pet"}} 432 expected := User{User: "cat", Roles: []string{"animal", "pet"}} 433 434 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true, PasswordStore: fastPasswordStore{}} 435 out, created, err := s.CreateOrUpdateUser(user) 436 if !created { 437 t.Error("Should have created user, instead updated?") 438 } 439 if err != nil { 440 t.Error("Unexpected error", err) 441 } 442 out.Password = "meow" 443 if !reflect.DeepEqual(out, user) { 444 t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected) 445 } 446 out, created, err = s.CreateOrUpdateUser(update) 447 if created { 448 t.Error("Should have updated user, instead created?") 449 } 450 if err != nil { 451 t.Error("Unexpected error", err) 452 } 453 if !reflect.DeepEqual(out, expected) { 454 t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected) 455 } 456} 457 458func TestUpdateRole(t *testing.T) { 459 oldrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}` 460 newrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": ["/animal"]}}}` 461 d := &testDoer{ 462 get: []etcdserver.Response{ 463 { 464 Event: &v2store.Event{ 465 Action: v2store.Get, 466 Node: &v2store.NodeExtern{ 467 Key: StorePermsPrefix + "/roles/animal", 468 Value: &oldrole, 469 }, 470 }, 471 }, 472 }, 473 put: []etcdserver.Response{ 474 { 475 Event: &v2store.Event{ 476 Action: v2store.Update, 477 Node: &v2store.NodeExtern{ 478 Key: StorePermsPrefix + "/roles/animal", 479 Value: &newrole, 480 }, 481 }, 482 }, 483 }, 484 explicitlyEnabled: true, 485 } 486 update := Role{Role: "animal", Grant: &Permissions{KV: RWPermission{Read: []string{}, Write: []string{"/animal"}}}} 487 expected := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{"/animal"}}}} 488 489 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} 490 out, err := s.UpdateRole(update) 491 if err != nil { 492 t.Error("Unexpected error", err) 493 } 494 if !reflect.DeepEqual(out, expected) { 495 t.Error("UpdateRole doesn't match given update. Got", out, "expected", expected) 496 } 497} 498 499func TestCreateRole(t *testing.T) { 500 role := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}` 501 d := &testDoer{ 502 put: []etcdserver.Response{ 503 { 504 Event: &v2store.Event{ 505 Action: v2store.Create, 506 Node: &v2store.NodeExtern{ 507 Key: StorePermsPrefix + "/roles/animal", 508 Value: &role, 509 }, 510 }, 511 }, 512 { 513 Event: nil, 514 }, 515 }, 516 explicitlyEnabled: true, 517 } 518 r := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{}}}} 519 520 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} 521 err := s.CreateRole(Role{Role: "root"}) 522 if err == nil { 523 t.Error("Should error creating root role") 524 } 525 err = s.CreateRole(r) 526 if err != nil { 527 t.Error("Unexpected error", err) 528 } 529 err = s.CreateRole(r) 530 if err == nil { 531 t.Error("Creating duplicate role, should error") 532 } 533} 534 535func TestEnableAuth(t *testing.T) { 536 rootUser := `{"user": "root", "password": ""}` 537 guestRole := `{"role": "guest", "permissions" : {"kv": {"read": ["*"], "write": ["*"]}}}` 538 trueval := "true" 539 falseval := "false" 540 d := &testDoer{ 541 get: []etcdserver.Response{ 542 { 543 Event: &v2store.Event{ 544 Action: v2store.Get, 545 Node: &v2store.NodeExtern{ 546 Key: StorePermsPrefix + "/enabled", 547 Value: &falseval, 548 }, 549 }, 550 }, 551 { 552 Event: &v2store.Event{ 553 Action: v2store.Get, 554 Node: &v2store.NodeExtern{ 555 Key: StorePermsPrefix + "/user/root", 556 Value: &rootUser, 557 }, 558 }, 559 }, 560 { 561 Event: nil, 562 }, 563 }, 564 put: []etcdserver.Response{ 565 { 566 Event: &v2store.Event{ 567 Action: v2store.Create, 568 Node: &v2store.NodeExtern{ 569 Key: StorePermsPrefix + "/roles/guest", 570 Value: &guestRole, 571 }, 572 }, 573 }, 574 { 575 Event: &v2store.Event{ 576 Action: v2store.Update, 577 Node: &v2store.NodeExtern{ 578 Key: StorePermsPrefix + "/enabled", 579 Value: &trueval, 580 }, 581 }, 582 }, 583 }, 584 explicitlyEnabled: false, 585 } 586 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} 587 err := s.EnableAuth() 588 if err != nil { 589 t.Error("Unexpected error", err) 590 } 591} 592 593func TestDisableAuth(t *testing.T) { 594 trueval := "true" 595 falseval := "false" 596 d := &testDoer{ 597 get: []etcdserver.Response{ 598 { 599 Event: &v2store.Event{ 600 Action: v2store.Get, 601 Node: &v2store.NodeExtern{ 602 Key: StorePermsPrefix + "/enabled", 603 Value: &falseval, 604 }, 605 }, 606 }, 607 { 608 Event: &v2store.Event{ 609 Action: v2store.Get, 610 Node: &v2store.NodeExtern{ 611 Key: StorePermsPrefix + "/enabled", 612 Value: &trueval, 613 }, 614 }, 615 }, 616 }, 617 put: []etcdserver.Response{ 618 { 619 Event: &v2store.Event{ 620 Action: v2store.Update, 621 Node: &v2store.NodeExtern{ 622 Key: StorePermsPrefix + "/enabled", 623 Value: &falseval, 624 }, 625 }, 626 }, 627 }, 628 explicitlyEnabled: false, 629 } 630 s := store{lg: zap.NewExample(), server: d, timeout: testTimeout, ensuredOnce: true} 631 err := s.DisableAuth() 632 if err == nil { 633 t.Error("Expected error; already disabled") 634 } 635 636 err = s.DisableAuth() 637 if err != nil { 638 t.Error("Unexpected error", err) 639 } 640} 641 642func TestSimpleMatch(t *testing.T) { 643 role := Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir/*", "/fookey"}, Write: []string{"/bardir/*", "/barkey"}}}} 644 if !role.HasKeyAccess("/foodir/foo/bar", false) { 645 t.Fatal("role lacks expected access") 646 } 647 if !role.HasKeyAccess("/fookey", false) { 648 t.Fatal("role lacks expected access") 649 } 650 if !role.HasRecursiveAccess("/foodir/*", false) { 651 t.Fatal("role lacks expected access") 652 } 653 if !role.HasRecursiveAccess("/foodir/foo*", false) { 654 t.Fatal("role lacks expected access") 655 } 656 if !role.HasRecursiveAccess("/bardir/*", true) { 657 t.Fatal("role lacks expected access") 658 } 659 if !role.HasKeyAccess("/bardir/bar/foo", true) { 660 t.Fatal("role lacks expected access") 661 } 662 if !role.HasKeyAccess("/barkey", true) { 663 t.Fatal("role lacks expected access") 664 } 665 666 if role.HasKeyAccess("/bardir/bar/foo", false) { 667 t.Fatal("role has unexpected access") 668 } 669 if role.HasKeyAccess("/barkey", false) { 670 t.Fatal("role has unexpected access") 671 } 672 if role.HasKeyAccess("/foodir/foo/bar", true) { 673 t.Fatal("role has unexpected access") 674 } 675 if role.HasKeyAccess("/fookey", true) { 676 t.Fatal("role has unexpected access") 677 } 678} 679