// 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 v2auth

import (
	"context"
	"reflect"
	"testing"
	"time"

	"go.etcd.io/etcd/api/v3/etcdserverpb"
	"go.etcd.io/etcd/server/v3/etcdserver"
	"go.etcd.io/etcd/server/v3/etcdserver/api/v2error"
	"go.etcd.io/etcd/server/v3/etcdserver/api/v2store"

	"go.uber.org/zap"
)

type fakeDoer struct{}

func (fakeDoer) Do(context.Context, etcdserverpb.Request) (etcdserver.Response, error) {
	return etcdserver.Response{}, nil
}

func TestCheckPassword(t *testing.T) {
	st := NewStore(zap.NewExample(), 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(zap.NewExample(), 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(zap.NewExample(), 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: &v2store.Event{
				Action: v2store.Get,
				Node: &v2store.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{}, &v2error.Error{
				ErrorCode: v2error.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{}, &v2error.Error{
				ErrorCode: v2error.EcodeNodeExist,
			}
		}
		td.putindex++
		return res, nil
	}
	return etcdserver.Response{}, nil
}

func TestAllUsers(t *testing.T) {
	d := &testDoer{
		get: []etcdserver.Response{
			{
				Event: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Nodes: v2store.NodeExterns([]*v2store.NodeExtern{
							{
								Key: StorePermsPrefix + "/users/cat",
							},
							{
								Key: StorePermsPrefix + "/users/dog",
							},
						}),
					},
				},
			},
		},
	}
	expected := []string{"cat", "dog"}

	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/users/cat",
						Value: &data,
					},
				},
			},
		},
		explicitlyEnabled: true,
	}
	expected := User{User: "cat", Roles: []string{"animal"}}

	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Nodes: v2store.NodeExterns([]*v2store.NodeExtern{
							{
								Key: StorePermsPrefix + "/roles/animal",
							},
							{
								Key: StorePermsPrefix + "/roles/human",
							},
						}),
					},
				},
			},
		},
		explicitlyEnabled: true,
	}
	expected := []string{"animal", "human", "root"}

	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/roles/animal",
						Value: &data,
					},
				},
			},
		},
		explicitlyEnabled: true,
	}
	expected := Role{Role: "animal"}

	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Set,
					Node: &v2store.NodeExtern{
						Key: StorePermsPrefix,
						Dir: true,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Set,
					Node: &v2store.NodeExtern{
						Key: StorePermsPrefix + "/users/",
						Dir: true,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Set,
					Node: &v2store.NodeExtern{
						Key: StorePermsPrefix + "/roles/",
						Dir: true,
					},
				},
			},
		},
	}

	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/users/cat",
						Value: &olduser,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/users/cat",
						Value: &olduser,
					},
				},
			},
		},
		put: []etcdserver.Response{
			{
				Event: &v2store.Event{
					Action: v2store.Update,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/users/cat",
						Value: &olduser,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Update,
					Node: &v2store.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{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/roles/animal",
						Value: &oldrole,
					},
				},
			},
		},
		put: []etcdserver.Response{
			{
				Event: &v2store.Event{
					Action: v2store.Update,
					Node: &v2store.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{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Create,
					Node: &v2store.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{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/enabled",
						Value: &falseval,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/user/root",
						Value: &rootUser,
					},
				},
			},
			{
				Event: nil,
			},
		},
		put: []etcdserver.Response{
			{
				Event: &v2store.Event{
					Action: v2store.Create,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/roles/guest",
						Value: &guestRole,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Update,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/enabled",
						Value: &trueval,
					},
				},
			},
		},
		explicitlyEnabled: false,
	}
	s := store{lg: zap.NewExample(), 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: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/enabled",
						Value: &falseval,
					},
				},
			},
			{
				Event: &v2store.Event{
					Action: v2store.Get,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/enabled",
						Value: &trueval,
					},
				},
			},
		},
		put: []etcdserver.Response{
			{
				Event: &v2store.Event{
					Action: v2store.Update,
					Node: &v2store.NodeExtern{
						Key:   StorePermsPrefix + "/enabled",
						Value: &falseval,
					},
				},
			},
		},
		explicitlyEnabled: false,
	}
	s := store{lg: zap.NewExample(), 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")
	}
}
