// Copyright 2023 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 etcdserver

import (
	"encoding/base64"
	"testing"
	"time"

	"golang.org/x/crypto/bcrypt"

	betesting "go.etcd.io/etcd/server/v3/mvcc/backend/testing"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap/zaptest"

	"go.etcd.io/etcd/api/v3/authpb"
	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
	"go.etcd.io/etcd/server/v3/auth"
	"go.etcd.io/etcd/server/v3/lease"
)

func TestCheckLeasePutsKeys(t *testing.T) {
	lg := zaptest.NewLogger(t)

	b, _ := betesting.NewDefaultTmpBackend(t)
	defer betesting.Close(t, b)

	simpleTokenTTLDefault := 300 * time.Second
	tokenTypeSimple := "simple"
	dummyIndexWaiter := func(index uint64) <-chan struct{} {
		ch := make(chan struct{}, 1)
		go func() {
			ch <- struct{}{}
		}()
		return ch
	}

	tp, _ := auth.NewTokenProvider(zaptest.NewLogger(t), tokenTypeSimple, dummyIndexWaiter, simpleTokenTTLDefault)
	as := auth.NewAuthStore(lg, b, tp, bcrypt.MinCost)

	aa := authApplierV3{as: as}
	assert.NoError(t, aa.checkLeasePutsKeys(lease.NewLease(lease.LeaseID(1), 3600)), "auth is disabled, should allow puts")
	assert.NoError(t, enableAuthAndCreateRoot(aa.as), "error while enabling auth")
	aa.authInfo = auth.AuthInfo{Username: "root"}
	assert.NoError(t, aa.checkLeasePutsKeys(lease.NewLease(lease.LeaseID(1), 3600)), "auth is enabled, should allow puts for root")

	l := lease.NewLease(lease.LeaseID(1), 3600)
	l.SetLeaseItem(lease.LeaseItem{Key: "a"})
	aa.authInfo = auth.AuthInfo{Username: "bob", Revision: 0}
	assert.ErrorIs(t, aa.checkLeasePutsKeys(l), auth.ErrUserEmpty, "auth is enabled, should not allow bob, non existing at rev 0")
	aa.authInfo = auth.AuthInfo{Username: "bob", Revision: 1}
	assert.ErrorIs(t, aa.checkLeasePutsKeys(l), auth.ErrAuthOldRevision, "auth is enabled, old revision")

	aa.authInfo = auth.AuthInfo{Username: "bob", Revision: aa.as.Revision()}
	assert.ErrorIs(t, aa.checkLeasePutsKeys(l), auth.ErrPermissionDenied, "auth is enabled, bob does not have permissions, bob does not exist")
	_, err := aa.as.UserAdd(&pb.AuthUserAddRequest{Name: "bob", Options: &authpb.UserAddOptions{NoPassword: true}})
	assert.NoError(t, err, "bob should be added without error")
	aa.authInfo = auth.AuthInfo{Username: "bob", Revision: aa.as.Revision()}
	assert.ErrorIs(t, aa.checkLeasePutsKeys(l), auth.ErrPermissionDenied, "auth is enabled, bob exists yet does not have permissions")

	// allow bob to access "a"
	_, err = aa.as.RoleAdd(&pb.AuthRoleAddRequest{Name: "bobsrole"})
	assert.NoError(t, err, "bobsrole should be added without error")
	_, err = aa.as.RoleGrantPermission(&pb.AuthRoleGrantPermissionRequest{
		Name: "bobsrole",
		Perm: &authpb.Permission{
			PermType: authpb.READWRITE,
			Key:      []byte("a"),
			RangeEnd: nil,
		},
	})
	assert.NoError(t, err, "bobsrole should be granted permissions without error")
	_, err = aa.as.UserGrantRole(&pb.AuthUserGrantRoleRequest{
		User: "bob",
		Role: "bobsrole",
	})
	assert.NoError(t, err, "bob should be granted bobsrole without error")

	aa.authInfo = auth.AuthInfo{Username: "bob", Revision: aa.as.Revision()}
	assert.NoError(t, aa.checkLeasePutsKeys(l), "bob should be able to access key 'a'")

}

func enableAuthAndCreateRoot(as auth.AuthStore) error {
	_, err := as.UserAdd(&pb.AuthUserAddRequest{
		Name:           "root",
		HashedPassword: encodePassword("root"),
		Options:        &authpb.UserAddOptions{NoPassword: false}})
	if err != nil {
		return err
	}

	_, err = as.RoleAdd(&pb.AuthRoleAddRequest{Name: "root"})
	if err != nil {
		return err
	}

	_, err = as.UserGrantRole(&pb.AuthUserGrantRoleRequest{User: "root", Role: "root"})
	if err != nil {
		return err
	}

	return as.AuthEnable()
}

func encodePassword(s string) string {
	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(s), bcrypt.MinCost)
	return base64.StdEncoding.EncodeToString(hashedPassword)
}
