// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package agent

import (
	"context"
	"fmt"
	"io"
	"testing"
	"time"

	"github.com/armon/go-metrics"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/serf/serf"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/acl/resolver"
	"github.com/hashicorp/consul/agent/config"
	"github.com/hashicorp/consul/agent/consul"
	"github.com/hashicorp/consul/agent/local"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/internal/gossip/librtt"
	"github.com/hashicorp/consul/lib"
	"github.com/hashicorp/consul/proto-public/pbresource"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/types"
)

type authzResolver func(string) (structs.ACLIdentity, acl.Authorizer, error)
type identResolver func(string) (structs.ACLIdentity, error)

type TestACLAgent struct {
	resolveAuthzFn authzResolver
	resolveIdentFn identResolver

	*Agent
}

// NewTestACLAgent does just enough so that all the code within agent/acl.go can work
// Basically it needs a local state for some of the vet* functions, a logger and a delegate.
// The key is that we are the delegate so we can control the ResolveToken responses
func NewTestACLAgent(t *testing.T, name string, hcl string, resolveAuthz authzResolver, resolveIdent identResolver) *TestACLAgent {
	t.Helper()

	if resolveIdent == nil {
		resolveIdent = func(s string) (structs.ACLIdentity, error) {
			return nil, nil
		}
	}

	a := &TestACLAgent{resolveAuthzFn: resolveAuthz, resolveIdentFn: resolveIdent}

	dataDir := testutil.TempDir(t, "acl-agent")

	logBuffer := testutil.NewLogBuffer(t)

	logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
		Name:       name,
		Level:      testutil.TestLogLevel,
		Output:     logBuffer,
		TimeFormat: "04:05.000",
	})

	loader := func(source config.Source) (config.LoadResult, error) {
		dataDir := fmt.Sprintf(`data_dir = "%s"`, dataDir)
		opts := config.LoadOpts{
			HCL:           []string{TestConfigHCL(NodeID()), hcl, dataDir},
			DefaultConfig: source,
		}
		result, err := config.Load(opts)
		if result.RuntimeConfig != nil {
			result.RuntimeConfig.Telemetry.Disable = true
		}
		return result, err
	}
	bd, err := NewBaseDeps(loader, logBuffer, logger)
	require.NoError(t, err)

	bd.MetricsConfig = &lib.MetricsConfig{
		Handler: metrics.NewInmemSink(1*time.Second, time.Minute),
	}

	agent, err := New(bd)
	require.NoError(t, err)

	agent.delegate = a
	agent.State = local.NewState(LocalConfig(bd.RuntimeConfig), bd.Logger, bd.Tokens)
	agent.State.TriggerSyncChanges = func() {}
	a.Agent = agent
	return a
}

func (a *TestACLAgent) ResolveToken(secretID string) (acl.Authorizer, error) {
	if a.resolveAuthzFn == nil {
		return nil, fmt.Errorf("ResolveToken call is unexpected - no authz resolver callback set")
	}

	_, authz, err := a.resolveAuthzFn(secretID)
	return authz, err
}

func (a *TestACLAgent) ResolveTokenAndDefaultMeta(secretID string, entMeta *acl.EnterpriseMeta, authzContext *acl.AuthorizerContext) (resolver.Result, error) {
	authz, err := a.ResolveToken(secretID)
	if err != nil {
		return resolver.Result{}, err
	}

	identity, err := a.resolveIdentFn(secretID)
	if err != nil {
		return resolver.Result{}, err
	}

	// Default the EnterpriseMeta based on the Tokens meta or actual defaults
	// in the case of unknown identity
	if identity != nil {
		entMeta.Merge(identity.EnterpriseMetadata())
	} else {
		entMeta.Merge(structs.DefaultEnterpriseMetaInDefaultPartition())
	}

	// Use the meta to fill in the ACL authorization context
	entMeta.FillAuthzContext(authzContext)

	return resolver.Result{Authorizer: authz, ACLIdentity: identity}, err
}

// All of these are stubs to satisfy the interface
func (a *TestACLAgent) GetLANCoordinate() (librtt.CoordinateSet, error) {
	return nil, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Leave() error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) LANMembersInAgentPartition() []serf.Member {
	return nil
}
func (a *TestACLAgent) LANMembers(f consul.LANMemberFilter) ([]serf.Member, error) {
	return nil, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) AgentLocalMember() serf.Member {
	return serf.Member{}
}
func (a *TestACLAgent) JoinLAN(addrs []string, entMeta *acl.EnterpriseMeta) (n int, err error) {
	return 0, fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) RemoveFailedNode(node string, prune bool, entMeta *acl.EnterpriseMeta) error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) RPC(ctx context.Context, method string, args interface{}, reply interface{}) error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) SnapshotRPC(args *structs.SnapshotRequest, in io.Reader, out io.Writer, replyFn structs.SnapshotReplyFn) error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Shutdown() error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) Stats() map[string]map[string]string {
	return nil
}
func (a *TestACLAgent) ReloadConfig(_ consul.ReloadableConfig) error {
	return fmt.Errorf("Unimplemented")
}
func (a *TestACLAgent) ResourceServiceClient() pbresource.ResourceServiceClient {
	return nil
}

func TestACL_Version8EnabledByDefault(t *testing.T) {
	t.Parallel()

	called := false
	resolveFn := func(string) (structs.ACLIdentity, acl.Authorizer, error) {
		called = true
		return nil, nil, acl.ErrNotFound
	}
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), resolveFn, nil)

	_, err := a.delegate.ResolveTokenAndDefaultMeta("nope", nil, nil)
	require.Error(t, err)
	require.True(t, called)
}

func authzFromPolicy(policy *acl.Policy, cfg *acl.Config) (acl.Authorizer, error) {
	return acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, cfg)
}

type testTokenRules struct {
	token structs.ACLToken
	// rules to create associated policy
	rules string
}

var (
	nodeROSecret    = "7e80d017-bccc-492f-8dec-65f03aeaebf3"
	nodeRWSecret    = "e3586ee5-02a2-4bf4-9ec3-9c4be7606e8c"
	serviceROSecret = "3d2c8552-df3b-4da7-9890-36885cbf56ac"
	serviceRWSecret = "4a1017a2-f788-4be3-93f2-90566f1340bb"
	otherRWSecret   = "a38e8016-91b6-4876-b3e7-a307abbb2002"

	testACLs = map[string]testTokenRules{
		nodeROSecret: {
			token: structs.ACLToken{
				AccessorID: "9df2d1a4-2d07-414e-8ead-6053f56ed2eb",
				SecretID:   nodeROSecret,
			},
			rules: `node_prefix "Node" { policy = "read" }`,
		},
		nodeRWSecret: {
			token: structs.ACLToken{
				AccessorID: "efb6b7d5-d343-47c1-b4cb-aa6b94d2f490",
				SecretID:   nodeRWSecret,
			},
			rules: `node_prefix "Node" { policy = "write" }`,
		},
		serviceROSecret: {
			token: structs.ACLToken{
				AccessorID: "0da53edb-36e5-4603-9c31-79965bad45f5",
				SecretID:   serviceROSecret,
			},
			rules: `service_prefix "service" { policy = "read" }`,
		},
		serviceRWSecret: {
			token: structs.ACLToken{
				AccessorID: "52504258-137a-41e6-9326-01f40e80872e",
				SecretID:   serviceRWSecret,
			},
			rules: `service_prefix "service" { policy = "write" }`,
		},
		otherRWSecret: {
			token: structs.ACLToken{
				AccessorID: "5e032c5b-c39e-4552-b5ad-8a9365b099c4",
				SecretID:   otherRWSecret,
			},
			rules: `service_prefix "other" { policy = "write" }`,
		},
	}
)

func catalogPolicy(testACL string) (structs.ACLIdentity, acl.Authorizer, error) {
	tok, ok := testACLs[testACL]
	if !ok {
		return nil, nil, acl.ErrNotFound
	}

	policy, err := acl.NewPolicyFromSource(tok.rules, nil, nil)
	if err != nil {
		return nil, nil, err
	}

	authz, err := authzFromPolicy(policy, nil)
	return &tok.token, authz, err
}

func catalogIdent(testACL string) (structs.ACLIdentity, error) {
	tok, ok := testACLs[testACL]
	if !ok {
		return nil, acl.ErrNotFound
	}

	return &tok.token, nil
}

func TestACL_vetServiceRegister(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	// Register a new service, with permission.
	err := a.vetServiceRegister(serviceRWSecret, &structs.NodeService{
		ID:      "my-service",
		Service: "service",
	})
	require.NoError(t, err)

	// Register a new service without write privs.
	err = a.vetServiceRegister(serviceROSecret, &structs.NodeService{
		ID:      "my-service",
		Service: "service",
	})
	require.True(t, acl.IsErrPermissionDenied(err))

	// Try to register over a service without write privs to the existing
	// service.
	a.State.AddServiceWithChecks(&structs.NodeService{
		ID:      "my-service",
		Service: "other",
	}, nil, "", false)
	err = a.vetServiceRegister(serviceRWSecret, &structs.NodeService{
		ID:      "my-service",
		Service: "service",
	})
	require.True(t, acl.IsErrPermissionDenied(err))
}

func TestACL_vetServiceUpdateWithAuthorizer(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	vetServiceUpdate := func(token string, serviceID structs.ServiceID) error {
		authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
		if err != nil {
			return err
		}

		return a.vetServiceUpdateWithAuthorizer(authz, serviceID)
	}

	// Update a service that doesn't exist.
	err := vetServiceUpdate(serviceRWSecret, structs.NewServiceID("my-service", nil))
	require.Error(t, err)
	require.Contains(t, err.Error(), "Unknown service")

	// Update with write privs.
	a.State.AddServiceWithChecks(&structs.NodeService{
		ID:      "my-service",
		Service: "service",
	}, nil, "", false)
	err = vetServiceUpdate(serviceRWSecret, structs.NewServiceID("my-service", nil))
	require.NoError(t, err)

	// Update without write privs.
	err = vetServiceUpdate(serviceROSecret, structs.NewServiceID("my-service", nil))
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))
}

func TestACL_vetCheckRegisterWithAuthorizer(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	vetCheckRegister := func(token string, check *structs.HealthCheck) error {
		authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
		if err != nil {
			return err
		}
		return a.vetCheckRegisterWithAuthorizer(authz, check)
	}

	// Register a new service check with write privs.
	err := vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
		CheckID:     types.CheckID("my-check"),
		ServiceID:   "my-service",
		ServiceName: "service",
	})
	require.NoError(t, err)

	// Register a new service check without write privs.
	err = vetCheckRegister(serviceROSecret, &structs.HealthCheck{
		CheckID:     types.CheckID("my-check"),
		ServiceID:   "my-service",
		ServiceName: "service",
	})
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Register a new node check with write privs.
	err = vetCheckRegister(nodeRWSecret, &structs.HealthCheck{
		CheckID: types.CheckID("my-check"),
	})
	require.NoError(t, err)

	// Register a new node check without write privs.
	err = vetCheckRegister(nodeROSecret, &structs.HealthCheck{
		CheckID: types.CheckID("my-check"),
	})
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Try to register over a service check without write privs to the
	// existing service.
	a.State.AddServiceWithChecks(&structs.NodeService{
		ID:      "my-service",
		Service: "service",
	}, nil, "", false)
	a.State.AddCheck(&structs.HealthCheck{
		CheckID:     types.CheckID("my-check"),
		ServiceID:   "my-service",
		ServiceName: "other",
	}, "", false)
	err = vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
		CheckID:     types.CheckID("my-check"),
		ServiceID:   "my-service",
		ServiceName: "service",
	})
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))

	// Try to register over a node check without write privs to the node.
	a.State.AddCheck(&structs.HealthCheck{
		CheckID: types.CheckID("my-node-check"),
	}, "", false)
	err = vetCheckRegister(serviceRWSecret, &structs.HealthCheck{
		CheckID:     types.CheckID("my-node-check"),
		ServiceID:   "my-service",
		ServiceName: "service",
	})
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))
}

func TestACL_vetCheckUpdateWithAuthorizer(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	vetCheckUpdate := func(token string, checkID structs.CheckID) error {
		authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
		if err != nil {
			return err
		}

		return a.vetCheckUpdateWithAuthorizer(authz, checkID)
	}

	// Update a check that doesn't exist.
	err := vetCheckUpdate(nodeRWSecret, structs.NewCheckID("my-check", nil))
	require.Error(t, err)
	require.Contains(t, err.Error(), "Unknown check")

	// Update service check with write privs.
	a.State.AddServiceWithChecks(&structs.NodeService{
		ID:      "my-service",
		Service: "service",
	}, nil, "", false)
	a.State.AddCheck(&structs.HealthCheck{
		CheckID:     types.CheckID("my-service-check"),
		ServiceID:   "my-service",
		ServiceName: "service",
	}, "", false)
	err = vetCheckUpdate(serviceRWSecret, structs.NewCheckID("my-service-check", nil))
	require.NoError(t, err)

	// Update service check without write privs.
	err = vetCheckUpdate(serviceROSecret, structs.NewCheckID("my-service-check", nil))
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err), "not permission denied: %s", err.Error())

	// Update node check with write privs.
	a.State.AddCheck(&structs.HealthCheck{
		CheckID: types.CheckID("my-node-check"),
	}, "", false)
	err = vetCheckUpdate(nodeRWSecret, structs.NewCheckID("my-node-check", nil))
	require.NoError(t, err)

	// Update without write privs.
	err = vetCheckUpdate(nodeROSecret, structs.NewCheckID("my-node-check", nil))
	require.Error(t, err)
	require.True(t, acl.IsErrPermissionDenied(err))
}

func TestACL_filterMembers(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	var members []serf.Member
	require.NoError(t, a.filterMembers(nodeROSecret, &members))
	require.Len(t, members, 0)

	members = []serf.Member{
		{Name: "Node 1"},
		{Name: "Nope"},
		{Name: "Node 2"},
	}
	require.NoError(t, a.filterMembers(nodeROSecret, &members))
	require.Len(t, members, 2)
	require.Equal(t, members[0].Name, "Node 1")
	require.Equal(t, members[1].Name, "Node 2")
}

func TestACL_filterServicesWithAuthorizer(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	filterServices := func(token string, services map[string]*api.AgentService) error {
		authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
		if err != nil {
			return err
		}

		return a.filterServicesWithAuthorizer(authz, services)
	}

	services := make(map[string]*api.AgentService)
	require.NoError(t, filterServices(nodeROSecret, services))

	services[structs.NewServiceID("my-service", nil).String()] = &api.AgentService{ID: "my-service", Service: "service"}
	services[structs.NewServiceID("my-other", nil).String()] = &api.AgentService{ID: "my-other", Service: "other"}
	require.NoError(t, filterServices(serviceROSecret, services))

	require.Contains(t, services, structs.NewServiceID("my-service", nil).String())
	require.NotContains(t, services, structs.NewServiceID("my-other", nil).String())
}

func TestACL_filterChecksWithAuthorizer(t *testing.T) {
	t.Parallel()
	a := NewTestACLAgent(t, t.Name(), TestACLConfig(), catalogPolicy, catalogIdent)

	filterChecks := func(token string, checks map[types.CheckID]*structs.HealthCheck) error {
		authz, err := a.delegate.ResolveTokenAndDefaultMeta(token, nil, nil)
		if err != nil {
			return err
		}

		return a.filterChecksWithAuthorizer(authz, checks)
	}

	checks := make(map[types.CheckID]*structs.HealthCheck)
	require.NoError(t, filterChecks(nodeROSecret, checks))

	checks["my-node"] = &structs.HealthCheck{}
	checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
	checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
	require.NoError(t, filterChecks(serviceROSecret, checks))
	_, ok := checks["my-node"]
	require.False(t, ok)
	_, ok = checks["my-service"]
	require.True(t, ok)
	_, ok = checks["my-other"]
	require.False(t, ok)

	checks["my-node"] = &structs.HealthCheck{}
	checks["my-service"] = &structs.HealthCheck{ServiceName: "service"}
	checks["my-other"] = &structs.HealthCheck{ServiceName: "other"}
	require.NoError(t, filterChecks(nodeROSecret, checks))
	_, ok = checks["my-node"]
	require.True(t, ok)
	_, ok = checks["my-service"]
	require.False(t, ok)
	_, ok = checks["my-other"]
	require.False(t, ok)
}
