blob: ad32260de090a2ab6032dcb31ccce64ed6bb4f19 [file] [log] [blame]
// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"reflect"
"testing"
"time"
etcderr "github.com/coreos/etcd/error"
"github.com/coreos/etcd/etcdserver"
"github.com/coreos/etcd/etcdserver/etcdserverpb"
etcdstore "github.com/coreos/etcd/store"
)
type fakeDoer struct{}
func (_ fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) {
return etcdserver.Response{}, nil
}
func TestCheckPassword(t *testing.T) {
st := NewStore(fakeDoer{}, 5*time.Second)
u := User{Password: "$2a$10$I3iddh1D..EIOXXQtsra4u8AjOtgEa2ERxVvYGfXFBJDo1omXwP.q"}
matched := st.CheckPassword(u, "foo")
if matched {
t.Fatalf("expected false, got %v", matched)
}
}
const testTimeout = time.Millisecond
func TestMergeUser(t *testing.T) {
tbl := []struct {
input User
merge User
expect User
iserr bool
}{
{
User{User: "foo"},
User{User: "bar"},
User{},
true,
},
{
User{User: "foo"},
User{User: "foo"},
User{User: "foo", Roles: []string{}},
false,
},
{
User{User: "foo"},
User{User: "foo", Grant: []string{"role1"}},
User{User: "foo", Roles: []string{"role1"}},
false,
},
{
User{User: "foo", Roles: []string{"role1"}},
User{User: "foo", Grant: []string{"role1"}},
User{},
true,
},
{
User{User: "foo", Roles: []string{"role1"}},
User{User: "foo", Revoke: []string{"role2"}},
User{},
true,
},
{
User{User: "foo", Roles: []string{"role1"}},
User{User: "foo", Grant: []string{"role2"}},
User{User: "foo", Roles: []string{"role1", "role2"}},
false,
},
{ // empty password will not overwrite the previous password
User{User: "foo", Password: "foo", Roles: []string{}},
User{User: "foo", Password: ""},
User{User: "foo", Password: "foo", Roles: []string{}},
false,
},
}
for i, tt := range tbl {
out, err := tt.input.merge(tt.merge, passwordStore{})
if err != nil && !tt.iserr {
t.Fatalf("Got unexpected error on item %d", i)
}
if !tt.iserr {
if !reflect.DeepEqual(out, tt.expect) {
t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
}
}
}
}
func TestMergeRole(t *testing.T) {
tbl := []struct {
input Role
merge Role
expect Role
iserr bool
}{
{
Role{Role: "foo"},
Role{Role: "bar"},
Role{},
true,
},
{
Role{Role: "foo"},
Role{Role: "foo", Grant: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
false,
},
{
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}, Write: []string{"/foodir"}}}},
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{}, Write: []string{}}}},
false,
},
{
Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/bardir"}}}},
Role{Role: "foo", Revoke: &Permissions{KV: RWPermission{Read: []string{"/foodir"}}}},
Role{},
true,
},
}
for i, tt := range tbl {
out, err := tt.input.merge(tt.merge)
if err != nil && !tt.iserr {
t.Fatalf("Got unexpected error on item %d", i)
}
if !tt.iserr {
if !reflect.DeepEqual(out, tt.expect) {
t.Errorf("Unequal merge expectation on item %d: got: %#v, expect: %#v", i, out, tt.expect)
}
}
}
}
type testDoer struct {
get []etcdserver.Response
put []etcdserver.Response
getindex int
putindex int
explicitlyEnabled bool
}
func (td *testDoer) Do(_ context.Context, req etcdserverpb.Request) (etcdserver.Response, error) {
if td.explicitlyEnabled && (req.Path == StorePermsPrefix+"/enabled") {
t := "true"
return etcdserver.Response{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &t,
},
},
}, nil
}
if (req.Method == "GET" || req.Method == "QGET") && td.get != nil {
res := td.get[td.getindex]
if res.Event == nil {
td.getindex++
return etcdserver.Response{}, &etcderr.Error{
ErrorCode: etcderr.EcodeKeyNotFound,
}
}
td.getindex++
return res, nil
}
if req.Method == "PUT" && td.put != nil {
res := td.put[td.putindex]
if res.Event == nil {
td.putindex++
return etcdserver.Response{}, &etcderr.Error{
ErrorCode: etcderr.EcodeNodeExist,
}
}
td.putindex++
return res, nil
}
return etcdserver.Response{}, nil
}
func TestAllUsers(t *testing.T) {
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Nodes: etcdstore.NodeExterns([]*etcdstore.NodeExtern{
{
Key: StorePermsPrefix + "/users/cat",
},
{
Key: StorePermsPrefix + "/users/dog",
},
}),
},
},
},
},
}
expected := []string{"cat", "dog"}
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
users, err := s.AllUsers()
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(users, expected) {
t.Error("AllUsers doesn't match given store. Got", users, "expected", expected)
}
}
func TestGetAndDeleteUser(t *testing.T) {
data := `{"user": "cat", "roles" : ["animal"]}`
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &data,
},
},
},
},
explicitlyEnabled: true,
}
expected := User{User: "cat", Roles: []string{"animal"}}
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
out, err := s.GetUser("cat")
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(out, expected) {
t.Error("GetUser doesn't match given store. Got", out, "expected", expected)
}
err = s.DeleteUser("cat")
if err != nil {
t.Error("Unexpected error", err)
}
}
func TestAllRoles(t *testing.T) {
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Nodes: etcdstore.NodeExterns([]*etcdstore.NodeExtern{
{
Key: StorePermsPrefix + "/roles/animal",
},
{
Key: StorePermsPrefix + "/roles/human",
},
}),
},
},
},
},
explicitlyEnabled: true,
}
expected := []string{"animal", "human", "root"}
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
out, err := s.AllRoles()
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(out, expected) {
t.Error("AllRoles doesn't match given store. Got", out, "expected", expected)
}
}
func TestGetAndDeleteRole(t *testing.T) {
data := `{"role": "animal"}`
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/animal",
Value: &data,
},
},
},
},
explicitlyEnabled: true,
}
expected := Role{Role: "animal"}
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
out, err := s.GetRole("animal")
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(out, expected) {
t.Error("GetRole doesn't match given store. Got", out, "expected", expected)
}
err = s.DeleteRole("animal")
if err != nil {
t.Error("Unexpected error", err)
}
}
func TestEnsure(t *testing.T) {
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Set,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix,
Dir: true,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Set,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/",
Dir: true,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Set,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/",
Dir: true,
},
},
},
},
}
s := store{server: d, timeout: testTimeout, ensuredOnce: false}
err := s.ensureAuthDirectories()
if err != nil {
t.Error("Unexpected error", err)
}
}
type fastPasswordStore struct {
}
func (_ fastPasswordStore) CheckPassword(user User, password string) bool {
return user.Password == password
}
func (_ fastPasswordStore) HashPassword(password string) (string, error) { return password, nil }
func TestCreateAndUpdateUser(t *testing.T) {
olduser := `{"user": "cat", "roles" : ["animal"]}`
newuser := `{"user": "cat", "roles" : ["animal", "pet"]}`
d := &testDoer{
get: []etcdserver.Response{
{
Event: nil,
},
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &olduser,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &olduser,
},
},
},
},
put: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Update,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &olduser,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Update,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/users/cat",
Value: &newuser,
},
},
},
},
explicitlyEnabled: true,
}
user := User{User: "cat", Password: "meow", Roles: []string{"animal"}}
update := User{User: "cat", Grant: []string{"pet"}}
expected := User{User: "cat", Roles: []string{"animal", "pet"}}
s := store{server: d, timeout: testTimeout, ensuredOnce: true, PasswordStore: fastPasswordStore{}}
out, created, err := s.CreateOrUpdateUser(user)
if !created {
t.Error("Should have created user, instead updated?")
}
if err != nil {
t.Error("Unexpected error", err)
}
out.Password = "meow"
if !reflect.DeepEqual(out, user) {
t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
}
out, created, err = s.CreateOrUpdateUser(update)
if created {
t.Error("Should have updated user, instead created?")
}
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(out, expected) {
t.Error("UpdateUser doesn't match given update. Got", out, "expected", expected)
}
}
func TestUpdateRole(t *testing.T) {
oldrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
newrole := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": ["/animal"]}}}`
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/animal",
Value: &oldrole,
},
},
},
},
put: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Update,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/animal",
Value: &newrole,
},
},
},
},
explicitlyEnabled: true,
}
update := Role{Role: "animal", Grant: &Permissions{KV: RWPermission{Read: []string{}, Write: []string{"/animal"}}}}
expected := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{"/animal"}}}}
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
out, err := s.UpdateRole(update)
if err != nil {
t.Error("Unexpected error", err)
}
if !reflect.DeepEqual(out, expected) {
t.Error("UpdateRole doesn't match given update. Got", out, "expected", expected)
}
}
func TestCreateRole(t *testing.T) {
role := `{"role": "animal", "permissions" : {"kv": {"read": ["/animal"], "write": []}}}`
d := &testDoer{
put: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Create,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/animal",
Value: &role,
},
},
},
{
Event: nil,
},
},
explicitlyEnabled: true,
}
r := Role{Role: "animal", Permissions: Permissions{KV: RWPermission{Read: []string{"/animal"}, Write: []string{}}}}
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
err := s.CreateRole(Role{Role: "root"})
if err == nil {
t.Error("Should error creating root role")
}
err = s.CreateRole(r)
if err != nil {
t.Error("Unexpected error", err)
}
err = s.CreateRole(r)
if err == nil {
t.Error("Creating duplicate role, should error")
}
}
func TestEnableAuth(t *testing.T) {
rootUser := `{"user": "root", "password": ""}`
guestRole := `{"role": "guest", "permissions" : {"kv": {"read": ["*"], "write": ["*"]}}}`
trueval := "true"
falseval := "false"
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/enabled",
Value: &falseval,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/user/root",
Value: &rootUser,
},
},
},
{
Event: nil,
},
},
put: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Create,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/roles/guest",
Value: &guestRole,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Update,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/enabled",
Value: &trueval,
},
},
},
},
explicitlyEnabled: false,
}
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
err := s.EnableAuth()
if err != nil {
t.Error("Unexpected error", err)
}
}
func TestDisableAuth(t *testing.T) {
trueval := "true"
falseval := "false"
d := &testDoer{
get: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/enabled",
Value: &falseval,
},
},
},
{
Event: &etcdstore.Event{
Action: etcdstore.Get,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/enabled",
Value: &trueval,
},
},
},
},
put: []etcdserver.Response{
{
Event: &etcdstore.Event{
Action: etcdstore.Update,
Node: &etcdstore.NodeExtern{
Key: StorePermsPrefix + "/enabled",
Value: &falseval,
},
},
},
},
explicitlyEnabled: false,
}
s := store{server: d, timeout: testTimeout, ensuredOnce: true}
err := s.DisableAuth()
if err == nil {
t.Error("Expected error; already disabled")
}
err = s.DisableAuth()
if err != nil {
t.Error("Unexpected error", err)
}
}
func TestSimpleMatch(t *testing.T) {
role := Role{Role: "foo", Permissions: Permissions{KV: RWPermission{Read: []string{"/foodir/*", "/fookey"}, Write: []string{"/bardir/*", "/barkey"}}}}
if !role.HasKeyAccess("/foodir/foo/bar", false) {
t.Fatal("role lacks expected access")
}
if !role.HasKeyAccess("/fookey", false) {
t.Fatal("role lacks expected access")
}
if !role.HasRecursiveAccess("/foodir/*", false) {
t.Fatal("role lacks expected access")
}
if !role.HasRecursiveAccess("/foodir/foo*", false) {
t.Fatal("role lacks expected access")
}
if !role.HasRecursiveAccess("/bardir/*", true) {
t.Fatal("role lacks expected access")
}
if !role.HasKeyAccess("/bardir/bar/foo", true) {
t.Fatal("role lacks expected access")
}
if !role.HasKeyAccess("/barkey", true) {
t.Fatal("role lacks expected access")
}
if role.HasKeyAccess("/bardir/bar/foo", false) {
t.Fatal("role has unexpected access")
}
if role.HasKeyAccess("/barkey", false) {
t.Fatal("role has unexpected access")
}
if role.HasKeyAccess("/foodir/foo/bar", true) {
t.Fatal("role has unexpected access")
}
if role.HasKeyAccess("/fookey", true) {
t.Fatal("role has unexpected access")
}
}