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

package agent

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v3/jwt"
	"github.com/stretchr/testify/require"

	"github.com/hashicorp/go-uuid"

	"github.com/hashicorp/consul/acl"
	"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
	"github.com/hashicorp/consul/agent/structs"
	"github.com/hashicorp/consul/api"
	"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
	"github.com/hashicorp/consul/sdk/testutil"
	"github.com/hashicorp/consul/testrpc"
)

// NOTE: The tests contained herein are designed to test the HTTP API
//       They are not intended to thoroughly test the backing RPC
//       functionality as that will be done with other tests.

func isHTTPBadRequest(err error) bool {
	if err, ok := err.(HTTPError); ok {
		return err.StatusCode == http.StatusBadRequest
	}
	return false
}

func TestACL_Disabled_Response(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, "")
	defer a.Shutdown()

	type testCase struct {
		name string
		fn   func(resp http.ResponseWriter, req *http.Request) (interface{}, error)
	}

	tests := []testCase{
		{"ACLBootstrap", a.srv.ACLBootstrap},
		{"ACLReplicationStatus", a.srv.ACLReplicationStatus},
		{"AgentToken", a.srv.AgentToken}, // See TestAgent_Token
		{"ACLPolicyList", a.srv.ACLPolicyList},
		{"ACLPolicyCRUD", a.srv.ACLPolicyCRUD},
		{"ACLPolicyCreate", a.srv.ACLPolicyCreate},
		{"ACLTokenList", a.srv.ACLTokenList},
		{"ACLTokenCreate", a.srv.ACLTokenCreate},
		{"ACLTokenSelf", a.srv.ACLTokenSelf},
		{"ACLTokenCRUD", a.srv.ACLTokenCRUD},
		{"ACLRoleList", a.srv.ACLRoleList},
		{"ACLRoleCreate", a.srv.ACLRoleCreate},
		{"ACLRoleCRUD", a.srv.ACLRoleCRUD},
		{"ACLBindingRuleList", a.srv.ACLBindingRuleList},
		{"ACLBindingRuleCreate", a.srv.ACLBindingRuleCreate},
		{"ACLBindingRuleCRUD", a.srv.ACLBindingRuleCRUD},
		{"ACLAuthMethodList", a.srv.ACLAuthMethodList},
		{"ACLAuthMethodCreate", a.srv.ACLAuthMethodCreate},
		{"ACLAuthMethodCRUD", a.srv.ACLAuthMethodCRUD},
		{"ACLLogin", a.srv.ACLLogin},
		{"ACLLogout", a.srv.ACLLogout},
		{"ACLAuthorize", a.srv.ACLAuthorize},
	}
	testrpc.WaitForLeader(t, a.RPC, "dc1")
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req, _ := http.NewRequest("PUT", "/should/not/care", nil)
			resp := httptest.NewRecorder()
			obj, err := tt.fn(resp, req)
			require.Nil(t, obj)
			require.ErrorIs(t, err, HTTPError{StatusCode: http.StatusUnauthorized, Reason: "ACL support disabled"})
		})
	}
}

func jsonBody(v interface{}) io.Reader {
	body := bytes.NewBuffer(nil)
	enc := json.NewEncoder(body)
	enc.Encode(v)
	return body
}

func TestACL_Bootstrap(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, `
		primary_datacenter = "dc1"

		acl {
			enabled = true
			default_policy = "deny"
		}
	`)
	defer a.Shutdown()

	tests := []struct {
		name   string
		method string
		code   int
		token  bool
	}{
		{"bootstrap", "PUT", http.StatusOK, true},
		{"not again", "PUT", http.StatusForbidden, false},
	}
	testrpc.WaitForLeader(t, a.RPC, "dc1")
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resp := httptest.NewRecorder()
			req, _ := http.NewRequest(tt.method, "/v1/acl/bootstrap", nil)
			out, err := a.srv.ACLBootstrap(resp, req)
			if tt.token && err != nil {
				t.Fatalf("err: %v", err)
			}
			if tt.token {
				wrap, ok := out.(*aclBootstrapResponse)
				if !ok {
					t.Fatalf("bad: %T", out)
				}
				if len(wrap.ID) != len("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") {
					t.Fatalf("bad: %v", wrap)
				}
				if wrap.ID != wrap.SecretID {
					t.Fatalf("bad: %v", wrap)
				}
			} else {
				if out != nil {
					t.Fatalf("bad: %T", out)
				}
			}
		})
	}
}

func TestACL_BootstrapWithToken(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, `
		primary_datacenter = "dc1"

		acl {
			enabled = true
			default_policy = "deny"
		}
	`)
	defer a.Shutdown()

	tests := []struct {
		name   string
		method string
		code   int
		token  bool
	}{
		{"bootstrap", "PUT", http.StatusOK, true},
		{"not again", "PUT", http.StatusForbidden, false},
	}
	testrpc.WaitForLeader(t, a.RPC, "dc1")
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var bootstrapSecret struct {
				BootstrapSecret string
			}
			bootstrapSecret.BootstrapSecret = "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a"
			resp := httptest.NewRecorder()
			req, _ := http.NewRequest(tt.method, "/v1/acl/bootstrap", jsonBody(bootstrapSecret))
			out, err := a.srv.ACLBootstrap(resp, req)
			if tt.token && err != nil {
				t.Fatalf("err: %v", err)
			}
			if tt.token {
				wrap, ok := out.(*aclBootstrapResponse)
				if !ok {
					t.Fatalf("bad: %T", out)
				}
				if wrap.ID != bootstrapSecret.BootstrapSecret {
					t.Fatalf("bad: %v", wrap)
				}
				if wrap.ID != wrap.SecretID {
					t.Fatalf("bad: %v", wrap)
				}
			} else {
				if out != nil {
					t.Fatalf("bad: %T", out)
				}
			}
		})
	}
}

func TestACL_HTTP(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, TestACLConfig())
	defer a.Shutdown()

	testrpc.WaitForLeader(t, a.RPC, "dc1")

	idMap := make(map[string]string)
	policyMap := make(map[string]*structs.ACLPolicy)
	roleMap := make(map[string]*structs.ACLRole)
	tokenMap := make(map[string]*structs.ACLToken)

	// This is all done as a subtest for a couple reasons
	// 1. It uses only 1 test agent and these are
	//    somewhat expensive to bring up and tear down often
	// 2. Instead of having to bring up a new agent and prime
	//    the ACL system with some data before running the test
	//    we can intelligently order these tests so we can still
	//    test everything with less actual operations and do
	//    so in a manner that is less prone to being flaky
	//
	// This could be accomplished with just blocks of code but I find
	// the go test output nicer to pinpoint the error if they are grouped.
	//
	// NOTE: None of the subtests should be parallelized in order for
	// any of it to work properly.
	t.Run("Policy", func(t *testing.T) {
		t.Run("Create", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				Name:        "test",
				Description: "test",
				Rules:       `acl = "read"`,
				Datacenters: []string{"dc1"},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy", jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLPolicyCreate(resp, req)
			require.NoError(t, err)

			policy, ok := obj.(*structs.ACLPolicy)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, policy.ID, 36)
			require.Equal(t, policyInput.Name, policy.Name)
			require.Equal(t, policyInput.Description, policy.Description)
			require.Equal(t, policyInput.Rules, policy.Rules)
			require.Equal(t, policyInput.Datacenters, policy.Datacenters)
			require.True(t, policy.CreateIndex > 0)
			require.Equal(t, policy.CreateIndex, policy.ModifyIndex)
			require.NotNil(t, policy.Hash)
			require.NotEqual(t, policy.Hash, []byte{})

			idMap["policy-test"] = policy.ID
			policyMap[policy.ID] = policy
		})

		t.Run("Minimal", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				Name:  "minimal",
				Rules: `key_prefix "" { policy = "read" }`,
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy", jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLPolicyCreate(resp, req)
			require.NoError(t, err)

			policy, ok := obj.(*structs.ACLPolicy)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, policy.ID, 36)
			require.Equal(t, policyInput.Name, policy.Name)
			require.Equal(t, policyInput.Description, policy.Description)
			require.Equal(t, policyInput.Rules, policy.Rules)
			require.Equal(t, policyInput.Datacenters, policy.Datacenters)
			require.True(t, policy.CreateIndex > 0)
			require.Equal(t, policy.CreateIndex, policy.ModifyIndex)
			require.NotNil(t, policy.Hash)
			require.NotEqual(t, policy.Hash, []byte{})

			idMap["policy-minimal"] = policy.ID
			policyMap[policy.ID] = policy
		})

		t.Run("Name Chars", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				Name:  "read-all_nodes-012",
				Rules: `node_prefix "" { policy = "read" }`,
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy", jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLPolicyCreate(resp, req)
			require.NoError(t, err)

			policy, ok := obj.(*structs.ACLPolicy)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, policy.ID, 36)
			require.Equal(t, policyInput.Name, policy.Name)
			require.Equal(t, policyInput.Description, policy.Description)
			require.Equal(t, policyInput.Rules, policy.Rules)
			require.Equal(t, policyInput.Datacenters, policy.Datacenters)
			require.True(t, policy.CreateIndex > 0)
			require.Equal(t, policy.CreateIndex, policy.ModifyIndex)
			require.NotNil(t, policy.Hash)
			require.NotEqual(t, policy.Hash, []byte{})

			idMap["policy-read-all-nodes"] = policy.ID
			policyMap[policy.ID] = policy
		})

		t.Run("Update Name ID Mismatch", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				ID:          "ac7560be-7f11-4d6d-bfcf-15633c2090fd",
				Name:        "read-all-nodes",
				Description: "Can read all node information",
				Rules:       `node_prefix "" { policy = "read" }`,
				Datacenters: []string{"dc1"},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy/"+idMap["policy-read-all-nodes"], jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLPolicyCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Policy CRUD Missing ID in URL", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/policy/", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLPolicyCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Update", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				Name:        "read-all-nodes",
				Description: "Can read all node information",
				Rules:       `node_prefix "" { policy = "read" }`,
				Datacenters: []string{"dc1"},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy/"+idMap["policy-read-all-nodes"], jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLPolicyCRUD(resp, req)
			require.NoError(t, err)

			policy, ok := obj.(*structs.ACLPolicy)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, policy.ID, 36)
			require.Equal(t, policyInput.Name, policy.Name)
			require.Equal(t, policyInput.Description, policy.Description)
			require.Equal(t, policyInput.Rules, policy.Rules)
			require.Equal(t, policyInput.Datacenters, policy.Datacenters)
			require.True(t, policy.CreateIndex > 0)
			require.True(t, policy.CreateIndex < policy.ModifyIndex)
			require.NotNil(t, policy.Hash)
			require.NotEqual(t, policy.Hash, []byte{})

			idMap["policy-read-all-nodes"] = policy.ID
			policyMap[policy.ID] = policy
		})

		t.Run("ID Supplied", func(t *testing.T) {
			policyInput := &structs.ACLPolicy{
				ID:          "12123d01-37f1-47e6-b55b-32328652bd38",
				Name:        "with-id",
				Description: "test",
				Rules:       `acl = "read"`,
				Datacenters: []string{"dc1"},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/policy", jsonBody(policyInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLPolicyCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Invalid payload", func(t *testing.T) {
			body := bytes.NewBuffer(nil)
			body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

			req, _ := http.NewRequest("PUT", "/v1/acl/policy", body)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLPolicyCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Delete", func(t *testing.T) {
			req, _ := http.NewRequest("DELETE", "/v1/acl/policy/"+idMap["policy-minimal"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLPolicyCRUD(resp, req)
			require.NoError(t, err)
			delete(policyMap, idMap["policy-minimal"])
			delete(idMap, "policy-minimal")
		})

		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/policies", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLPolicyList(resp, req)
			require.NoError(t, err)
			policies, ok := raw.(structs.ACLPolicyListStubs)
			require.True(t, ok)

			// 2 we just created + builtin policies
			require.Len(t, policies, 2+len(structs.ACLBuiltinPolicies))

			for policyID, expected := range policyMap {
				found := false
				for _, actual := range policies {
					if actual.ID == policyID {
						require.Equal(t, expected.Name, actual.Name)
						require.Equal(t, expected.Datacenters, actual.Datacenters)
						require.Equal(t, expected.Hash, actual.Hash)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}

				require.True(t, found)
			}
		})

		t.Run("Read", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/policy/"+idMap["policy-read-all-nodes"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLPolicyCRUD(resp, req)
			require.NoError(t, err)
			policy, ok := raw.(*structs.ACLPolicy)
			require.True(t, ok)
			require.Equal(t, policyMap[idMap["policy-read-all-nodes"]], policy)
		})

		t.Run("Read Name", func(t *testing.T) {
			policyName := "read-all-nodes"
			req, _ := http.NewRequest("GET", "/v1/acl/policy/name/"+policyName, nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLPolicyReadByName(resp, req)
			require.NoError(t, err)
			policy, ok := raw.(*structs.ACLPolicy)
			require.True(t, ok)
			require.Equal(t, policyMap[idMap["policy-"+policyName]], policy)
		})
	})

	t.Run("Role", func(t *testing.T) {
		t.Run("Create", func(t *testing.T) {
			roleInput := &structs.ACLRole{
				Name:        "test",
				Description: "test",
				Policies: []structs.ACLRolePolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "web-node",
						Datacenter: "foo",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/role", jsonBody(roleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLRoleCreate(resp, req)
			require.NoError(t, err)

			role, ok := obj.(*structs.ACLRole)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, role.ID, 36)
			require.Equal(t, roleInput.Name, role.Name)
			require.Equal(t, roleInput.Description, role.Description)
			require.Equal(t, roleInput.Policies, role.Policies)
			require.Equal(t, roleInput.NodeIdentities, role.NodeIdentities)
			require.True(t, role.CreateIndex > 0)
			require.Equal(t, role.CreateIndex, role.ModifyIndex)
			require.NotNil(t, role.Hash)
			require.NotEqual(t, role.Hash, []byte{})

			idMap["role-test"] = role.ID
			roleMap[role.ID] = role
		})

		t.Run("Name Chars", func(t *testing.T) {
			roleInput := &structs.ACLRole{
				Name: "service-id-web",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "web",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/role", jsonBody(roleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLRoleCreate(resp, req)
			require.NoError(t, err)

			role, ok := obj.(*structs.ACLRole)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, role.ID, 36)
			require.Equal(t, roleInput.Name, role.Name)
			require.Equal(t, roleInput.Description, role.Description)
			require.Equal(t, roleInput.ServiceIdentities, role.ServiceIdentities)
			require.True(t, role.CreateIndex > 0)
			require.Equal(t, role.CreateIndex, role.ModifyIndex)
			require.NotNil(t, role.Hash)
			require.NotEqual(t, role.Hash, []byte{})

			idMap["role-service-id-web"] = role.ID
			roleMap[role.ID] = role
		})

		t.Run("Update Name ID Mismatch", func(t *testing.T) {
			roleInput := &structs.ACLRole{
				ID:          "ac7560be-7f11-4d6d-bfcf-15633c2090fd",
				Name:        "test",
				Description: "test",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "db",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/role/"+idMap["role-test"], jsonBody(roleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLRoleCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Role CRUD Missing ID in URL", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/role/", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLRoleCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Update", func(t *testing.T) {
			roleInput := &structs.ACLRole{
				Name:        "test",
				Description: "test",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "web-indexer",
					},
				},
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "web-node",
						Datacenter: "foo",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/role/"+idMap["role-test"], jsonBody(roleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLRoleCRUD(resp, req)
			require.NoError(t, err)

			role, ok := obj.(*structs.ACLRole)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, role.ID, 36)
			require.Equal(t, roleInput.Name, role.Name)
			require.Equal(t, roleInput.Description, role.Description)
			require.Equal(t, roleInput.Policies, role.Policies)
			require.Equal(t, roleInput.ServiceIdentities, role.ServiceIdentities)
			require.Equal(t, roleInput.NodeIdentities, role.NodeIdentities)
			require.True(t, role.CreateIndex > 0)
			require.True(t, role.CreateIndex < role.ModifyIndex)
			require.NotNil(t, role.Hash)
			require.NotEqual(t, role.Hash, []byte{})

			idMap["role-test"] = role.ID
			roleMap[role.ID] = role
		})

		t.Run("ID Supplied", func(t *testing.T) {
			roleInput := &structs.ACLRole{
				ID:          "12123d01-37f1-47e6-b55b-32328652bd38",
				Name:        "with-id",
				Description: "test",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "foobar",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/role", jsonBody(roleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLRoleCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Invalid payload", func(t *testing.T) {
			body := bytes.NewBuffer(nil)
			body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

			req, _ := http.NewRequest("PUT", "/v1/acl/role", body)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLRoleCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Delete", func(t *testing.T) {
			req, _ := http.NewRequest("DELETE", "/v1/acl/role/"+idMap["role-service-id-web"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLRoleCRUD(resp, req)
			require.NoError(t, err)
			delete(roleMap, idMap["role-service-id-web"])
			delete(idMap, "role-service-id-web")
		})

		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/roles", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLRoleList(resp, req)
			require.NoError(t, err)
			roles, ok := raw.(structs.ACLRoles)
			require.True(t, ok)

			// 1 we just created
			require.Len(t, roles, 1)

			for roleID, expected := range roleMap {
				found := false
				for _, actual := range roles {
					if actual.ID == roleID {
						require.Equal(t, expected.Name, actual.Name)
						require.Equal(t, expected.Policies, actual.Policies)
						require.Equal(t, expected.ServiceIdentities, actual.ServiceIdentities)
						require.Equal(t, expected.Hash, actual.Hash)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}

				require.True(t, found)
			}
		})

		t.Run("Read", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/role/"+idMap["role-test"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLRoleCRUD(resp, req)
			require.NoError(t, err)
			role, ok := raw.(*structs.ACLRole)
			require.True(t, ok)
			require.Equal(t, roleMap[idMap["role-test"]], role)
		})
	})

	t.Run("Token", func(t *testing.T) {
		t.Run("Create", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "foo",
						Datacenter: "bar",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, token.AccessorID, 36)
			require.Len(t, token.SecretID, 36)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.Equal(t, tokenInput.NodeIdentities, token.NodeIdentities)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})
		t.Run("Create Local", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "local",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
				Local: true,
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, token.AccessorID, 36)
			require.Len(t, token.SecretID, 36)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-local"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})
		t.Run("Read", func(t *testing.T) {
			expected := tokenMap[idMap["token-test"]]
			req, _ := http.NewRequest("GET", "/v1/acl/token/"+expected.AccessorID, nil)
			req.Header.Add("X-Consul-Token", "root")

			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)
			require.Equal(t, expected, token)
		})
		t.Run("Read-expanded", func(t *testing.T) {
			expected := tokenMap[idMap["token-test"]]
			req, _ := http.NewRequest("GET", "/v1/acl/token/"+expected.AccessorID+"?expanded=true", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			tokenResp, ok := obj.(*structs.ACLTokenExpanded)
			require.True(t, ok)
			require.Equal(t, expected, tokenResp.ACLToken)
			require.Len(t, tokenResp.ExpandedPolicies, 3)
		})
		t.Run("Self", func(t *testing.T) {
			expected := tokenMap[idMap["token-test"]]
			req, _ := http.NewRequest("GET", "/v1/acl/token/self", nil)
			req.Header.Add("X-Consul-Token", expected.SecretID)
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenSelf(resp, req)
			require.NoError(t, err)
			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)
			require.Equal(t, expected, token)
		})
		t.Run("Clone", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "cloned token",
			}

			baseToken := tokenMap[idMap["token-test"]]

			req, _ := http.NewRequest("PUT", "/v1/acl/token/"+baseToken.AccessorID+"/clone", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			require.NotEqual(t, baseToken.AccessorID, token.AccessorID)
			require.NotEqual(t, baseToken.SecretID, token.SecretID)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, baseToken.Policies, token.Policies)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-cloned"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})
		t.Run("Update", func(t *testing.T) {
			originalToken := tokenMap[idMap["token-cloned"]]

			// Secret will be filled in
			tokenInput := &structs.ACLToken{
				AccessorID:  tokenMap[idMap["token-cloned"]].AccessorID,
				Description: "Better description for this cloned token",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "foo",
						Datacenter: "bar",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token/"+originalToken.AccessorID, jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			require.Equal(t, originalToken.AccessorID, token.AccessorID)
			require.Equal(t, originalToken.SecretID, token.SecretID)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.Equal(t, tokenInput.NodeIdentities, token.NodeIdentities)
			require.True(t, token.CreateIndex > 0)
			require.True(t, token.CreateIndex < token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})
			require.NotEqual(t, token.Hash, originalToken.Hash)

			tokenMap[token.AccessorID] = token
		})

		t.Run("Update without AccessorID in request body", func(t *testing.T) {
			originalToken := tokenMap[idMap["token-cloned"]]

			// Secret will be filled in
			tokenInput := &structs.ACLToken{
				Description: "Even Better description for this cloned token",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "foo",
						Datacenter: "bar",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token/"+originalToken.AccessorID, jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			require.Equal(t, originalToken.AccessorID, token.AccessorID)
			require.Equal(t, originalToken.SecretID, token.SecretID)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.Equal(t, tokenInput.NodeIdentities, token.NodeIdentities)
			require.True(t, token.CreateIndex > 0)
			require.True(t, token.CreateIndex < token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})
			require.NotEqual(t, token.Hash, originalToken.Hash)

			tokenMap[token.AccessorID] = token
		})

		t.Run("CRUD Missing Token Accessor ID", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/token/", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.Error(t, err)
			require.Nil(t, obj)
			require.True(t, isHTTPBadRequest(err))
		})
		t.Run("Update Accessor Mismatch", func(t *testing.T) {
			originalToken := tokenMap[idMap["token-cloned"]]

			// Accessor and Secret will be filled in
			tokenInput := &structs.ACLToken{
				AccessorID:  "e8aeb69a-0ace-42b9-b95f-d1d9eafe1561",
				Description: "Better description for this cloned token",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token/"+originalToken.AccessorID, jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCRUD(resp, req)
			require.Error(t, err)
			require.Nil(t, obj)
			require.True(t, isHTTPBadRequest(err))
		})
		t.Run("Delete", func(t *testing.T) {
			req, _ := http.NewRequest("DELETE", "/v1/acl/token/"+idMap["token-cloned"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCRUD(resp, req)
			require.NoError(t, err)
			delete(tokenMap, idMap["token-cloned"])
			delete(idMap, "token-cloned")
		})
		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/tokens", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)

			// 3 tokens created but 1 was deleted + initial management token + anon token
			require.Len(t, tokens, 4)

			// this loop doesn't verify anything about the initial management token
			for tokenID, expected := range tokenMap {
				found := false
				for _, actual := range tokens {
					if actual.AccessorID == tokenID {
						require.Equal(t, expected.SecretID, actual.SecretID)
						require.Equal(t, expected.Description, actual.Description)
						require.Equal(t, expected.Policies, actual.Policies)
						require.Equal(t, expected.Local, actual.Local)
						require.Equal(t, expected.CreateTime, actual.CreateTime)
						require.Equal(t, expected.Hash, actual.Hash)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}
				require.True(t, found)
			}
		})
		t.Run("List by Policy", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/tokens?policy="+structs.ACLPolicyGlobalManagementID, nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)
			require.Len(t, tokens, 1)
			token := tokens[0]
			require.Equal(t, "Initial Management Token", token.Description)
			require.Len(t, token.Policies, 1)
			require.Equal(t, structs.ACLPolicyGlobalManagementID, token.Policies[0].ID)
		})
		t.Run("Create with Accessor", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				AccessorID:  "56e8e6a3-708b-4a2f-8ab3-b973cce39108",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Equal(t, tokenInput.AccessorID, token.AccessorID)
			require.Len(t, token.SecretID, 36)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})

		t.Run("Create with Secret", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				SecretID:    "4e3efd15-d06c-442e-a7cc-1744f55c8dea",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Equal(t, tokenInput.SecretID, token.SecretID)
			require.Len(t, token.AccessorID, 36)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})

		t.Run("Create with Accessor and Secret", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				AccessorID:  "dee863fa-e548-4c61-a96f-9aa07999249f",
				SecretID:    "10126ffa-b28f-4137-b9a9-e89ab1e97c5b",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Equal(t, tokenInput.SecretID, token.SecretID)
			require.Equal(t, tokenInput.AccessorID, token.AccessorID)
			require.Equal(t, tokenInput.Description, token.Description)
			require.Equal(t, tokenInput.Policies, token.Policies)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})

		t.Run("Create with Accessor Dup", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				AccessorID:  "dee863fa-e548-4c61-a96f-9aa07999249f",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with Secret as Accessor Dup", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				SecretID:    "dee863fa-e548-4c61-a96f-9aa07999249f",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with Secret Dup", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				SecretID:    "10126ffa-b28f-4137-b9a9-e89ab1e97c5b",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with Accessor as Secret Dup", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				AccessorID:  "10126ffa-b28f-4137-b9a9-e89ab1e97c5b",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with Reserved Accessor", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				AccessorID:  "00000000-0000-0000-0000-00000000005b",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with Reserved Secret", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				SecretID:    "00000000-0000-0000-0000-00000000005b",
				Description: "test",
				Policies: []structs.ACLTokenPolicyLink{
					{
						ID:   idMap["policy-test"],
						Name: policyMap[idMap["policy-test"]].Name,
					},
					{
						ID:   idMap["policy-read-all-nodes"],
						Name: policyMap[idMap["policy-read-all-nodes"]].Name,
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
		})

		t.Run("Create with uppercase node identity", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "agent token for foo node",
				NodeIdentities: []*structs.ACLNodeIdentity{
					{
						NodeName:   "FOO",
						Datacenter: "bar",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Only lowercase alphanumeric")
		})

		t.Run("Create with uppercase service identity", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "token for service identity foo",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "FOO",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.Error(t, err)
			testutil.RequireErrorContains(t, err, "Only lowercase alphanumeric")
		})

		t.Run("Create with valid service identity", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "token for service identity sn1",
				ServiceIdentities: []*structs.ACLServiceIdentity{
					{
						ServiceName: "sn1",
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)
		})

		t.Run("List by ServiceName", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/tokens?servicename=sn1", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)
			require.Len(t, tokens, 1)
			token := tokens[0]
			require.Equal(t, "token for service identity sn1", token.Description)
			require.Len(t, token.ServiceIdentities, 1)
			require.Equal(t, "sn1", token.ServiceIdentities[0].ServiceName)
		})

		t.Run("List by ServiceName based on templated policies", func(t *testing.T) {
			tokenInput := &structs.ACLToken{
				Description: "token for templated policies service",
				TemplatedPolicies: []*structs.ACLTemplatedPolicy{
					{
						TemplateName: "builtin/service",
						TemplateVariables: &structs.ACLTemplatedPolicyVariables{
							Name: "service1",
						},
					},
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/token", jsonBody(tokenInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCreate(resp, req)
			require.NoError(t, err)

			req, _ = http.NewRequest("GET", "/v1/acl/tokens?servicename=service1", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp = httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)
			require.Len(t, tokens, 1)
			token := tokens[0]
			require.Equal(t, "token for templated policies service", token.Description)
			require.Len(t, token.TemplatedPolicies, 1)
			require.Equal(t, "service1", token.TemplatedPolicies[0].TemplateVariables.Name)
		})
	})

	t.Run("ACLTemplatedPolicy", func(t *testing.T) {
		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/templated-policies", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			a.srv.h.ServeHTTP(resp, req)

			require.Equal(t, http.StatusOK, resp.Code)

			var list map[string]api.ACLTemplatedPolicyResponse
			require.NoError(t, json.NewDecoder(resp.Body).Decode(&list))
			require.Len(t, list, 6)

			require.Equal(t, api.ACLTemplatedPolicyResponse{
				TemplateName: api.ACLTemplatedPolicyServiceName,
				Schema:       structs.ACLTemplatedPolicyServiceSchema,
				Template:     structs.ACLTemplatedPolicyService,
				Description:  structs.ACLTemplatedPolicyServiceDescription,
			}, list[api.ACLTemplatedPolicyServiceName])
		})
		t.Run("Read", func(t *testing.T) {
			t.Run("With non existing templated policy", func(t *testing.T) {
				req, _ := http.NewRequest("GET", "/v1/acl/templated-policy/name/fake", nil)
				req.Header.Add("X-Consul-Token", "root")
				resp := httptest.NewRecorder()
				a.srv.h.ServeHTTP(resp, req)
				require.Equal(t, http.StatusBadRequest, resp.Code)
			})

			t.Run("With existing templated policy", func(t *testing.T) {
				req, _ := http.NewRequest("GET", "/v1/acl/templated-policy/name/"+api.ACLTemplatedPolicyDNSName, nil)
				req.Header.Add("X-Consul-Token", "root")
				resp := httptest.NewRecorder()

				a.srv.h.ServeHTTP(resp, req)
				require.Equal(t, http.StatusOK, resp.Code)

				var templatedPolicy api.ACLTemplatedPolicyResponse
				require.NoError(t, json.NewDecoder(resp.Body).Decode(&templatedPolicy))
				require.Equal(t, structs.ACLTemplatedPolicyNoRequiredVariablesSchema, templatedPolicy.Schema)
				require.Equal(t, structs.ACLTemplatedPolicyDNSDescription, templatedPolicy.Description)
				require.Equal(t, api.ACLTemplatedPolicyDNSName, templatedPolicy.TemplateName)
				require.Equal(t, structs.ACLTemplatedPolicyDNS, templatedPolicy.Template)
			})
		})
		t.Run("preview", func(t *testing.T) {
			t.Run("When missing required variables", func(t *testing.T) {
				previewInput := &structs.ACLTemplatedPolicyVariables{}
				req, _ := http.NewRequest(
					"POST",
					fmt.Sprintf("/v1/acl/templated-policy/preview/%s", api.ACLTemplatedPolicyServiceName),
					jsonBody(previewInput),
				)
				req.Header.Add("X-Consul-Token", "root")
				resp := httptest.NewRecorder()

				a.srv.h.ServeHTTP(resp, req)
				require.Equal(t, http.StatusBadRequest, resp.Code)
			})

			t.Run("Correct input", func(t *testing.T) {
				previewInput := &structs.ACLTemplatedPolicyVariables{Name: "web"}
				req, _ := http.NewRequest(
					"POST",
					fmt.Sprintf("/v1/acl/templated-policy/preview/%s", api.ACLTemplatedPolicyServiceName),
					jsonBody(previewInput),
				)
				req.Header.Add("X-Consul-Token", "root")
				resp := httptest.NewRecorder()

				a.srv.h.ServeHTTP(resp, req)
				require.Equal(t, http.StatusOK, resp.Code)

				var syntheticPolicy *structs.ACLPolicy
				require.NoError(t, json.NewDecoder(resp.Body).Decode(&syntheticPolicy))

				require.NotEmpty(t, syntheticPolicy.ID)
				require.NotEmpty(t, syntheticPolicy.Hash)
				require.Equal(t, "synthetic policy generated from templated policy: builtin/service", syntheticPolicy.Description)
				require.Contains(t, syntheticPolicy.Name, "synthetic-policy-")
			})
		})
	})
}

func TestACL_LoginProcedure_HTTP(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	// This tests AuthMethods, BindingRules, Login, and Logout.
	t.Parallel()
	a := NewTestAgent(t, TestACLConfig())
	defer a.Shutdown()

	testrpc.WaitForLeader(t, a.RPC, "dc1")

	idMap := make(map[string]string)
	methodMap := make(map[string]*structs.ACLAuthMethod)
	ruleMap := make(map[string]*structs.ACLBindingRule)
	tokenMap := make(map[string]*structs.ACLToken)

	testSessionID := testauth.StartSession()
	defer testauth.ResetSession(testSessionID)

	// This is all done as a subtest for a couple reasons
	// 1. It uses only 1 test agent and these are
	//    somewhat expensive to bring up and tear down often
	// 2. Instead of having to bring up a new agent and prime
	//    the ACL system with some data before running the test
	//    we can intelligently order these tests so we can still
	//    test everything with less actual operations and do
	//    so in a manner that is less prone to being flaky
	// 3. While this test will be large it should
	t.Run("AuthMethod", func(t *testing.T) {
		t.Run("Create", func(t *testing.T) {
			methodInput := &structs.ACLAuthMethod{
				Name:        "test",
				Type:        "testing",
				Description: "test",
				Config: map[string]interface{}{
					"SessionID": testSessionID,
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method", jsonBody(methodInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLAuthMethodCreate(resp, req)
			require.NoError(t, err)

			method, ok := obj.(*structs.ACLAuthMethod)
			require.True(t, ok)

			require.Equal(t, methodInput.Name, method.Name)
			require.Equal(t, methodInput.Type, method.Type)
			require.Equal(t, methodInput.Description, method.Description)
			require.Equal(t, methodInput.Config, method.Config)
			require.True(t, method.CreateIndex > 0)
			require.Equal(t, method.CreateIndex, method.ModifyIndex)

			methodMap[method.Name] = method
		})

		t.Run("Create other", func(t *testing.T) {
			methodInput := &structs.ACLAuthMethod{
				Name:        "other",
				Type:        "testing",
				Description: "test",
				Config: map[string]interface{}{
					"SessionID": testSessionID,
				},
				TokenLocality: "global",
				MaxTokenTTL:   500_000_000_000,
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method", jsonBody(methodInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLAuthMethodCreate(resp, req)
			require.NoError(t, err)

			method, ok := obj.(*structs.ACLAuthMethod)
			require.True(t, ok)

			require.Equal(t, methodInput.Name, method.Name)
			require.Equal(t, methodInput.Type, method.Type)
			require.Equal(t, methodInput.Description, method.Description)
			require.Equal(t, methodInput.Config, method.Config)
			require.True(t, method.CreateIndex > 0)
			require.Equal(t, method.CreateIndex, method.ModifyIndex)

			methodMap[method.Name] = method
		})

		t.Run("Create in remote datacenter", func(t *testing.T) {
			methodInput := &structs.ACLAuthMethod{
				Name:        "other",
				Type:        "testing",
				Description: "test",
				Config: map[string]interface{}{
					"SessionID": testSessionID,
				},
				TokenLocality: "global",
				MaxTokenTTL:   500_000_000_000,
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method?dc=remote", jsonBody(methodInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLAuthMethodCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Update Name URL Mismatch", func(t *testing.T) {
			methodInput := &structs.ACLAuthMethod{
				Name:        "test",
				Type:        "testing",
				Description: "test",
				Config: map[string]interface{}{
					"SessionID": testSessionID,
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method/not-test", jsonBody(methodInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLAuthMethodCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Update", func(t *testing.T) {
			methodInput := &structs.ACLAuthMethod{
				Name:        "test",
				Type:        "testing",
				Description: "updated description",
				Config: map[string]interface{}{
					"SessionID": testSessionID,
				},
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method/test", jsonBody(methodInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLAuthMethodCRUD(resp, req)
			require.NoError(t, err)

			method, ok := obj.(*structs.ACLAuthMethod)
			require.True(t, ok)

			require.Equal(t, methodInput.Name, method.Name)
			require.Equal(t, methodInput.Type, method.Type)
			require.Equal(t, methodInput.Description, method.Description)
			require.Equal(t, methodInput.Config, method.Config)
			require.True(t, method.CreateIndex > 0)
			require.True(t, method.CreateIndex < method.ModifyIndex)

			methodMap[method.Name] = method
		})

		t.Run("Invalid payload", func(t *testing.T) {
			body := bytes.NewBuffer(nil)
			body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

			req, _ := http.NewRequest("PUT", "/v1/acl/auth-method", body)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLAuthMethodCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/auth-methods", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLAuthMethodList(resp, req)
			require.NoError(t, err)

			methods, ok := raw.(structs.ACLAuthMethodListStubs)
			require.True(t, ok)

			// 2 we just created
			require.Len(t, methods, 2)

			for methodName, expected := range methodMap {
				found := false
				for _, actual := range methods {
					if actual.Name == methodName {
						require.Equal(t, expected.Name, actual.Name)
						require.Equal(t, expected.Type, actual.Type)
						require.Equal(t, expected.Description, actual.Description)
						require.Equal(t, expected.MaxTokenTTL, actual.MaxTokenTTL)
						require.Equal(t, expected.TokenLocality, actual.TokenLocality)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}

				require.True(t, found)
			}
		})

		t.Run("Delete", func(t *testing.T) {
			req, _ := http.NewRequest("DELETE", "/v1/acl/auth-method/other", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLAuthMethodCRUD(resp, req)
			require.NoError(t, err)
			delete(methodMap, "other")
		})

		t.Run("Read", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/auth-method/test", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLAuthMethodCRUD(resp, req)
			require.NoError(t, err)
			method, ok := raw.(*structs.ACLAuthMethod)
			require.True(t, ok)
			require.Equal(t, methodMap["test"], method)
		})
	})

	t.Run("BindingRule", func(t *testing.T) {
		t.Run("Create", func(t *testing.T) {
			ruleInput := &structs.ACLBindingRule{
				Description: "test",
				AuthMethod:  "test",
				Selector:    "serviceaccount.namespace==default",
				BindType:    structs.BindingRuleBindTypeService,
				BindName:    "web",
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule", jsonBody(ruleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLBindingRuleCreate(resp, req)
			require.NoError(t, err)

			rule, ok := obj.(*structs.ACLBindingRule)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, rule.ID, 36)
			require.Equal(t, ruleInput.Description, rule.Description)
			require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
			require.Equal(t, ruleInput.Selector, rule.Selector)
			require.Equal(t, ruleInput.BindType, rule.BindType)
			require.Equal(t, ruleInput.BindName, rule.BindName)
			require.True(t, rule.CreateIndex > 0)
			require.Equal(t, rule.CreateIndex, rule.ModifyIndex)

			idMap["rule-test"] = rule.ID
			ruleMap[rule.ID] = rule
		})

		t.Run("Create other", func(t *testing.T) {
			ruleInput := &structs.ACLBindingRule{
				Description: "other",
				AuthMethod:  "test",
				Selector:    "serviceaccount.namespace==default",
				BindType:    structs.BindingRuleBindTypeRole,
				BindName:    "fancy-role",
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule", jsonBody(ruleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLBindingRuleCreate(resp, req)
			require.NoError(t, err)

			rule, ok := obj.(*structs.ACLBindingRule)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, rule.ID, 36)
			require.Equal(t, ruleInput.Description, rule.Description)
			require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
			require.Equal(t, ruleInput.Selector, rule.Selector)
			require.Equal(t, ruleInput.BindType, rule.BindType)
			require.Equal(t, ruleInput.BindName, rule.BindName)
			require.True(t, rule.CreateIndex > 0)
			require.Equal(t, rule.CreateIndex, rule.ModifyIndex)

			idMap["rule-other"] = rule.ID
			ruleMap[rule.ID] = rule
		})

		t.Run("Create in remote datacenter", func(t *testing.T) {
			ruleInput := &structs.ACLBindingRule{
				Description: "other",
				AuthMethod:  "test",
				Selector:    "serviceaccount.namespace==default",
				BindType:    structs.BindingRuleBindTypeRole,
				BindName:    "fancy-role",
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule?dc=remote", jsonBody(ruleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLBindingRuleCRUD(resp, req)
			require.EqualError(t, err, "No path to datacenter")
		})

		t.Run("BindingRule CRUD Missing ID in URL", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/binding-rule/", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLBindingRuleCRUD(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Update", func(t *testing.T) {
			ruleInput := &structs.ACLBindingRule{
				Description: "updated",
				AuthMethod:  "test",
				Selector:    "serviceaccount.namespace==default",
				BindType:    structs.BindingRuleBindTypeService,
				BindName:    "${serviceaccount.name}",
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule/"+idMap["rule-test"], jsonBody(ruleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLBindingRuleCRUD(resp, req)
			require.NoError(t, err)

			rule, ok := obj.(*structs.ACLBindingRule)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, rule.ID, 36)
			require.Equal(t, ruleInput.Description, rule.Description)
			require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
			require.Equal(t, ruleInput.Selector, rule.Selector)
			require.Equal(t, ruleInput.BindType, rule.BindType)
			require.Equal(t, ruleInput.BindName, rule.BindName)
			require.True(t, rule.CreateIndex > 0)
			require.True(t, rule.CreateIndex < rule.ModifyIndex)

			idMap["rule-test"] = rule.ID
			ruleMap[rule.ID] = rule
		})

		t.Run("ID Supplied", func(t *testing.T) {
			ruleInput := &structs.ACLBindingRule{
				ID:          "12123d01-37f1-47e6-b55b-32328652bd38",
				Description: "with-id",
				AuthMethod:  "test",
				Selector:    "serviceaccount.namespace==default",
				BindType:    structs.BindingRuleBindTypeService,
				BindName:    "vault",
			}

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule", jsonBody(ruleInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLBindingRuleCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("Invalid payload", func(t *testing.T) {
			body := bytes.NewBuffer(nil)
			body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

			req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule", body)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLBindingRuleCreate(resp, req)
			require.Error(t, err)
			require.True(t, isHTTPBadRequest(err))
		})

		t.Run("List", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/binding-rules", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLBindingRuleList(resp, req)
			require.NoError(t, err)
			rules, ok := raw.(structs.ACLBindingRules)
			require.True(t, ok)

			// 2 we just created
			require.Len(t, rules, 2)

			for ruleID, expected := range ruleMap {
				found := false
				for _, actual := range rules {
					if actual.ID == ruleID {
						require.Equal(t, expected.Description, actual.Description)
						require.Equal(t, expected.AuthMethod, actual.AuthMethod)
						require.Equal(t, expected.Selector, actual.Selector)
						require.Equal(t, expected.BindType, actual.BindType)
						require.Equal(t, expected.BindName, actual.BindName)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}

				require.True(t, found)
			}
		})

		t.Run("Delete", func(t *testing.T) {
			req, _ := http.NewRequest("DELETE", "/v1/acl/binding-rule/"+idMap["rule-other"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLBindingRuleCRUD(resp, req)
			require.NoError(t, err)
			delete(ruleMap, idMap["rule-other"])
			delete(idMap, "rule-other")
		})

		t.Run("Read", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/binding-rule/"+idMap["rule-test"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLBindingRuleCRUD(resp, req)
			require.NoError(t, err)
			rule, ok := raw.(*structs.ACLBindingRule)
			require.True(t, ok)
			require.Equal(t, ruleMap[idMap["rule-test"]], rule)
		})
	})

	testauth.InstallSessionToken(testSessionID, "token1", "default", "demo1", "abc123")
	testauth.InstallSessionToken(testSessionID, "token2", "default", "demo2", "def456")

	t.Run("Login", func(t *testing.T) {
		t.Run("Create Token 1", func(t *testing.T) {
			loginInput := &structs.ACLLoginParams{
				AuthMethod:  "test",
				BearerToken: "token1",
				Meta:        map[string]string{"foo": "bar"},
			}

			req, _ := http.NewRequest("POST", "/v1/acl/login", jsonBody(loginInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLLogin(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, token.AccessorID, 36)
			require.Len(t, token.SecretID, 36)
			require.Equal(t, `token created via login: {"foo":"bar"}`, token.Description)
			require.True(t, token.Local)
			require.Len(t, token.Policies, 0)
			require.Len(t, token.Roles, 0)
			require.Len(t, token.ServiceIdentities, 1)
			require.Equal(t, "demo1", token.ServiceIdentities[0].ServiceName)
			require.Len(t, token.ServiceIdentities[0].Datacenters, 0)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test-1"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})
		t.Run("Create Token 2", func(t *testing.T) {
			loginInput := &structs.ACLLoginParams{
				AuthMethod:  "test",
				BearerToken: "token2",
				Meta:        map[string]string{"blah": "woot"},
			}

			req, _ := http.NewRequest("POST", "/v1/acl/login", jsonBody(loginInput))
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			obj, err := a.srv.ACLLogin(resp, req)
			require.NoError(t, err)

			token, ok := obj.(*structs.ACLToken)
			require.True(t, ok)

			// 36 = length of the string form of uuids
			require.Len(t, token.AccessorID, 36)
			require.Len(t, token.SecretID, 36)
			require.Equal(t, `token created via login: {"blah":"woot"}`, token.Description)
			require.True(t, token.Local)
			require.Len(t, token.Policies, 0)
			require.Len(t, token.Roles, 0)
			require.Len(t, token.ServiceIdentities, 1)
			require.Equal(t, "demo2", token.ServiceIdentities[0].ServiceName)
			require.Len(t, token.ServiceIdentities[0].Datacenters, 0)
			require.True(t, token.CreateIndex > 0)
			require.Equal(t, token.CreateIndex, token.ModifyIndex)
			require.NotNil(t, token.Hash)
			require.NotEqual(t, token.Hash, []byte{})

			idMap["token-test-2"] = token.AccessorID
			tokenMap[token.AccessorID] = token
		})

		t.Run("List Tokens by (incorrect) Method", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/tokens?authmethod=other", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)
			require.Len(t, tokens, 0)
		})

		t.Run("List Tokens by (correct) Method", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/tokens?authmethod=test", nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			raw, err := a.srv.ACLTokenList(resp, req)
			require.NoError(t, err)
			tokens, ok := raw.(structs.ACLTokenListStubs)
			require.True(t, ok)
			require.Len(t, tokens, 2)

			for tokenID, expected := range tokenMap {
				found := false
				for _, actual := range tokens {
					if actual.AccessorID == tokenID {
						require.Equal(t, expected.Description, actual.Description)
						require.Equal(t, expected.Policies, actual.Policies)
						require.Equal(t, expected.Roles, actual.Roles)
						require.Equal(t, expected.ServiceIdentities, actual.ServiceIdentities)
						require.Equal(t, expected.Local, actual.Local)
						require.Equal(t, expected.CreateTime, actual.CreateTime)
						require.Equal(t, expected.Hash, actual.Hash)
						require.Equal(t, expected.CreateIndex, actual.CreateIndex)
						require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
						found = true
						break
					}
				}
				require.True(t, found)
			}
		})

		t.Run("Logout", func(t *testing.T) {
			tok := tokenMap[idMap["token-test-1"]]
			req, _ := http.NewRequest("POST", "/v1/acl/logout", nil)
			req.Header.Add("X-Consul-Token", tok.SecretID)
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLLogout(resp, req)
			require.NoError(t, err)
		})

		t.Run("Token is gone after Logout", func(t *testing.T) {
			req, _ := http.NewRequest("GET", "/v1/acl/token/"+idMap["token-test-1"], nil)
			req.Header.Add("X-Consul-Token", "root")
			resp := httptest.NewRecorder()
			_, err := a.srv.ACLTokenCRUD(resp, req)
			require.Error(t, err)
			require.ErrorContains(t, err, acl.ErrNotFound.Error())
		})
	})
}

func TestACLEndpoint_LoginLogout_jwt(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()

	a := NewTestAgent(t, TestACLConfigWithParams(nil))
	defer a.Shutdown()

	testrpc.WaitForLeader(t, a.RPC, "dc1")

	// spin up a fake oidc server
	oidcServer := oidcauthtest.Start(t)
	pubKey, privKey := oidcServer.SigningKeys()

	type mConfig = map[string]interface{}
	cases := map[string]struct {
		f         func(config mConfig)
		issuer    string
		expectErr string
	}{
		"success - jwt static keys": {func(config mConfig) {
			config["BoundIssuer"] = "https://legit.issuer.internal/"
			config["JWTValidationPubKeys"] = []string{pubKey}
		},
			"https://legit.issuer.internal/",
			""},
		"success - jwt jwks": {func(config mConfig) {
			config["JWKSURL"] = oidcServer.Addr() + "/certs"
			config["JWKSCACert"] = oidcServer.CACert()
		},
			"https://legit.issuer.internal/",
			""},
		"success - jwt oidc discovery": {func(config mConfig) {
			config["OIDCDiscoveryURL"] = oidcServer.Addr()
			config["OIDCDiscoveryCACert"] = oidcServer.CACert()
		},
			oidcServer.Addr(),
			""},
	}

	for name, tc := range cases {
		tc := tc
		t.Run(name, func(t *testing.T) {
			method, err := upsertTestCustomizedAuthMethod(a.RPC, TestDefaultInitialManagementToken, "dc1", func(method *structs.ACLAuthMethod) {
				method.Type = "jwt"
				method.Config = map[string]interface{}{
					"JWTSupportedAlgs": []string{"ES256"},
					"ClaimMappings": map[string]string{
						"first_name":   "name",
						"/org/primary": "primary_org",
					},
					"ListClaimMappings": map[string]string{
						"https://consul.test/groups": "groups",
					},
					"BoundAudiences": []string{"https://consul.test"},
				}
				if tc.f != nil {
					tc.f(method.Config)
				}
			})
			require.NoError(t, err)

			t.Run("invalid bearer token", func(t *testing.T) {
				loginInput := &structs.ACLLoginParams{
					AuthMethod:  method.Name,
					BearerToken: "invalid",
				}

				req, _ := http.NewRequest("POST", "/v1/acl/login", jsonBody(loginInput))
				resp := httptest.NewRecorder()
				_, err := a.srv.ACLLogin(resp, req)
				require.Error(t, err)
			})

			cl := jwt.Claims{
				Subject:   "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients",
				Audience:  jwt.Audience{"https://consul.test"},
				Issuer:    tc.issuer,
				NotBefore: jwt.NewNumericDate(time.Now().Add(-5 * time.Second)),
				Expiry:    jwt.NewNumericDate(time.Now().Add(5 * time.Second)),
			}

			type orgs struct {
				Primary string `json:"primary"`
			}

			privateCl := struct {
				FirstName string   `json:"first_name"`
				Org       orgs     `json:"org"`
				Groups    []string `json:"https://consul.test/groups"`
			}{
				FirstName: "jeff2",
				Org:       orgs{"engineering"},
				Groups:    []string{"foo", "bar"},
			}

			jwtData, err := oidcauthtest.SignJWT(privKey, cl, privateCl)
			require.NoError(t, err)

			t.Run("valid bearer token no bindings", func(t *testing.T) {
				loginInput := &structs.ACLLoginParams{
					AuthMethod:  method.Name,
					BearerToken: jwtData,
				}

				req, _ := http.NewRequest("POST", "/v1/acl/login", jsonBody(loginInput))
				resp := httptest.NewRecorder()
				_, err := a.srv.ACLLogin(resp, req)

				testutil.RequireErrorContains(t, err, "Permission denied")
			})

			_, err = upsertTestCustomizedBindingRule(a.RPC, TestDefaultInitialManagementToken, "dc1", func(rule *structs.ACLBindingRule) {
				rule.AuthMethod = method.Name
				rule.BindType = structs.BindingRuleBindTypeService
				rule.BindName = "test--${value.name}--${value.primary_org}"
				rule.Selector = "value.name == jeff2 and value.primary_org == engineering and foo in list.groups"
			})
			require.NoError(t, err)

			t.Run("valid bearer token 1 service binding", func(t *testing.T) {
				loginInput := &structs.ACLLoginParams{
					AuthMethod:  method.Name,
					BearerToken: jwtData,
				}

				req, _ := http.NewRequest("POST", "/v1/acl/login", jsonBody(loginInput))
				resp := httptest.NewRecorder()
				obj, err := a.srv.ACLLogin(resp, req)
				require.NoError(t, err)

				token, ok := obj.(*structs.ACLToken)
				require.True(t, ok)

				require.Equal(t, method.Name, token.AuthMethod)
				require.Equal(t, `token created via login`, token.Description)
				require.True(t, token.Local)
				require.Len(t, token.Roles, 0)
				require.Len(t, token.ServiceIdentities, 1)
				svcid := token.ServiceIdentities[0]
				require.Len(t, svcid.Datacenters, 0)
				require.Equal(t, "test--jeff2--engineering", svcid.ServiceName)

				// and delete it
				req, _ = http.NewRequest("GET", "/v1/acl/logout", nil)
				req.Header.Add("X-Consul-Token", token.SecretID)
				resp = httptest.NewRecorder()
				_, err = a.srv.ACLLogout(resp, req)
				require.NoError(t, err)

				// verify the token was deleted
				req, _ = http.NewRequest("GET", "/v1/acl/token/"+token.AccessorID, nil)
				req.Header.Add("X-Consul-Token", TestDefaultInitialManagementToken)
				resp = httptest.NewRecorder()

				// make the request
				_, err = a.srv.ACLTokenCRUD(resp, req)
				require.Error(t, err)
				require.ErrorContains(t, err, acl.ErrNotFound.Error())
			})
		})
	}
}

func TestACL_Authorize(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a1 := NewTestAgent(t, TestACLConfigWithParams(nil), TestAgentOpts{DisableACLBootstrapCheck: true})
	defer a1.Shutdown()

	testrpc.WaitForTestAgent(t, a1.RPC, "dc1", testrpc.WithToken(TestDefaultInitialManagementToken))

	policyReq := structs.ACLPolicySetRequest{
		Policy: structs.ACLPolicy{
			Name:  "test",
			Rules: `acl = "read" operator = "write" service_prefix "" { policy = "read"} node_prefix "" { policy= "write" } key_prefix "/foo" { policy = "write" } `,
		},
		Datacenter:   "dc1",
		WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
	}
	var policy structs.ACLPolicy
	require.NoError(t, a1.RPC(context.Background(), "ACL.PolicySet", &policyReq, &policy))

	tokenReq := structs.ACLTokenSetRequest{
		ACLToken: structs.ACLToken{
			Policies: []structs.ACLTokenPolicyLink{
				{
					ID: policy.ID,
				},
			},
		},
		Datacenter:   "dc1",
		WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
	}

	var token structs.ACLToken
	require.NoError(t, a1.RPC(context.Background(), "ACL.TokenSet", &tokenReq, &token))

	// secondary also needs to setup a replication token to pull tokens and policies
	secondaryParams := DefaultTestACLConfigParams()
	secondaryParams.ReplicationToken = secondaryParams.InitialManagementToken
	secondaryParams.EnableTokenReplication = true

	a2 := NewTestAgent(t, `datacenter = "dc2" `+TestACLConfigWithParams(secondaryParams), TestAgentOpts{DisableACLBootstrapCheck: true})
	defer a2.Shutdown()

	addr := fmt.Sprintf("127.0.0.1:%d", a1.Config.SerfPortWAN)
	_, err := a2.JoinWAN([]string{addr})
	require.NoError(t, err)

	testrpc.WaitForTestAgent(t, a2.RPC, "dc2", testrpc.WithToken(TestDefaultInitialManagementToken))
	// this actually ensures a few things. First the dcs got connect okay, secondly that the policy we
	// are about ready to use in our local token creation exists in the secondary DC
	testrpc.WaitForACLReplication(t, a2.RPC, "dc2", structs.ACLReplicateTokens, policy.CreateIndex, 1, 0)

	localTokenReq := structs.ACLTokenSetRequest{
		ACLToken: structs.ACLToken{
			Policies: []structs.ACLTokenPolicyLink{
				{
					ID: policy.ID,
				},
			},
			Local: true,
		},
		Datacenter:   "dc2",
		WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
	}

	var localToken structs.ACLToken
	require.NoError(t, a2.RPC(context.Background(), "ACL.TokenSet", &localTokenReq, &localToken))

	t.Run("initial-management-token", func(t *testing.T) {
		request := []structs.ACLAuthorizationRequest{
			{
				Resource: "acl",
				Access:   "read",
			},
			{
				Resource: "acl",
				Access:   "write",
			},
			{
				Resource: "agent",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "agent",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "event",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "event",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "intention",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "intention",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "key",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "key",
				Segment:  "foo",
				Access:   "list",
			},
			{
				Resource: "key",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "keyring",
				Access:   "read",
			},
			{
				Resource: "keyring",
				Access:   "write",
			},
			{
				Resource: "node",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "node",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "operator",
				Access:   "read",
			},
			{
				Resource: "operator",
				Access:   "write",
			},
			{
				Resource: "mesh",
				Access:   "read",
			},
			{
				Resource: "mesh",
				Access:   "write",
			},
			{
				Resource: "peering",
				Access:   "read",
			},
			{
				Resource: "peering",
				Access:   "write",
			},
			{
				Resource: "query",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "query",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "service",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "service",
				Segment:  "foo",
				Access:   "write",
			},
			{
				Resource: "session",
				Segment:  "foo",
				Access:   "read",
			},
			{
				Resource: "session",
				Segment:  "foo",
				Access:   "write",
			},
		}

		for _, dc := range []string{"dc1", "dc2"} {
			t.Run(dc, func(t *testing.T) {
				req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize?dc="+dc, jsonBody(request))
				req.Header.Add("X-Consul-Token", TestDefaultInitialManagementToken)
				recorder := httptest.NewRecorder()
				raw, err := a1.srv.ACLAuthorize(recorder, req)
				require.NoError(t, err)
				responses, ok := raw.([]structs.ACLAuthorizationResponse)
				require.True(t, ok)
				require.Len(t, responses, len(request))

				for idx, req := range request {
					resp := responses[idx]

					require.Equal(t, req, resp.ACLAuthorizationRequest)
					require.True(t, resp.Allow, "should have allowed all access for initial management token")
				}
			})
		}

	})

	customAuthorizationRequests := []structs.ACLAuthorizationRequest{
		{
			Resource: "acl",
			Access:   "read",
		},
		{
			Resource: "acl",
			Access:   "write",
		},
		{
			Resource: "agent",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "agent",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "event",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "event",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "intention",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "intention",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "key",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "key",
			Segment:  "foo",
			Access:   "list",
		},
		{
			Resource: "key",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "keyring",
			Access:   "read",
		},
		{
			Resource: "keyring",
			Access:   "write",
		},
		{
			Resource: "node",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "node",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "operator",
			Access:   "read",
		},
		{
			Resource: "operator",
			Access:   "write",
		},
		{
			Resource: "mesh",
			Access:   "read",
		},
		{
			Resource: "mesh",
			Access:   "write",
		},
		{
			Resource: "peering",
			Access:   "read",
		},
		{
			Resource: "peering",
			Access:   "write",
		},
		{
			Resource: "query",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "query",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "service",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "service",
			Segment:  "foo",
			Access:   "write",
		},
		{
			Resource: "session",
			Segment:  "foo",
			Access:   "read",
		},
		{
			Resource: "session",
			Segment:  "foo",
			Access:   "write",
		},
	}

	expectedCustomAuthorizationResponses := []bool{
		true,  // acl:read
		false, // acl:write
		false, // agent:read
		false, // agent:write
		false, // event:read
		false, // event:write
		true,  // intentions:read
		false, // intention:write
		false, // key:read
		false, // key:list
		false, // key:write
		false, // keyring:read
		false, // keyring:write
		true,  // node:read
		true,  // node:write
		true,  // operator:read
		true,  // operator:write
		true,  // mesh:read
		true,  // mesh:write
		true,  // peering:read
		true,  // peering:write
		false, // query:read
		false, // query:write
		true,  // service:read
		false, // service:write
		false, // session:read
		false, // session:write
	}

	t.Run("custom-token", func(t *testing.T) {
		for _, dc := range []string{"dc1", "dc2"} {
			t.Run(dc, func(t *testing.T) {
				req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(customAuthorizationRequests))
				req.Header.Add("X-Consul-Token", token.SecretID)
				recorder := httptest.NewRecorder()
				raw, err := a1.srv.ACLAuthorize(recorder, req)
				require.NoError(t, err)
				responses, ok := raw.([]structs.ACLAuthorizationResponse)
				require.True(t, ok)
				require.Len(t, responses, len(customAuthorizationRequests))
				require.Len(t, responses, len(expectedCustomAuthorizationResponses))

				for idx, req := range customAuthorizationRequests {
					resp := responses[idx]

					require.Equal(t, req, resp.ACLAuthorizationRequest)
					require.Equal(t, expectedCustomAuthorizationResponses[idx], resp.Allow, "request %d - %+v returned unexpected response", idx, resp.ACLAuthorizationRequest)
				}
			})
		}
	})

	t.Run("too-many-requests", func(t *testing.T) {
		var request []structs.ACLAuthorizationRequest

		for i := 0; i < 100; i++ {
			request = append(request, structs.ACLAuthorizationRequest{Resource: "acl", Access: "read"})
		}

		req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
		req.Header.Add("X-Consul-Token", token.SecretID)
		recorder := httptest.NewRecorder()
		raw, err := a1.srv.ACLAuthorize(recorder, req)
		require.Error(t, err)
		require.Contains(t, err.Error(), "Refusing to process more than 64 authorizations at once")
		require.Nil(t, raw)
	})

	t.Run("decode-failure", func(t *testing.T) {
		req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(structs.ACLAuthorizationRequest{Resource: "acl", Access: "read"}))
		req.Header.Add("X-Consul-Token", token.SecretID)
		recorder := httptest.NewRecorder()
		raw, err := a1.srv.ACLAuthorize(recorder, req)
		require.Error(t, err)
		require.Contains(t, err.Error(), "Failed to decode request body")
		require.Nil(t, raw)
	})

	t.Run("acl-not-found", func(t *testing.T) {
		request := []structs.ACLAuthorizationRequest{
			{
				Resource: "acl",
				Access:   "read",
			},
		}

		req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
		req.Header.Add("X-Consul-Token", "d908c0be-22e1-433e-84db-8718e1a019de")
		recorder := httptest.NewRecorder()
		raw, err := a1.srv.ACLAuthorize(recorder, req)
		require.Error(t, err)
		require.Equal(t, acl.ErrNotFound, err)
		require.Nil(t, raw)
	})

	t.Run("local-token-in-secondary-dc", func(t *testing.T) {
		req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize?dc=dc2", jsonBody(customAuthorizationRequests))
		req.Header.Add("X-Consul-Token", localToken.SecretID)
		recorder := httptest.NewRecorder()
		raw, err := a1.srv.ACLAuthorize(recorder, req)
		require.NoError(t, err)
		responses, ok := raw.([]structs.ACLAuthorizationResponse)
		require.True(t, ok)
		require.Len(t, responses, len(customAuthorizationRequests))
		require.Len(t, responses, len(expectedCustomAuthorizationResponses))

		for idx, req := range customAuthorizationRequests {
			resp := responses[idx]

			require.Equal(t, req, resp.ACLAuthorizationRequest)
			require.Equal(t, expectedCustomAuthorizationResponses[idx], resp.Allow, "request %d - %+v returned unexpected response", idx, resp.ACLAuthorizationRequest)
		}
	})

	t.Run("local-token-wrong-dc", func(t *testing.T) {
		request := []structs.ACLAuthorizationRequest{
			{
				Resource: "acl",
				Access:   "read",
			},
		}

		req, _ := http.NewRequest("POST", "/v1/internal/acl/authorize", jsonBody(request))
		req.Header.Add("X-Consul-Token", localToken.SecretID)
		recorder := httptest.NewRecorder()
		raw, err := a1.srv.ACLAuthorize(recorder, req)
		require.Error(t, err)
		require.Equal(t, acl.ErrNotFound, err)
		require.Nil(t, raw)
	})
}

type rpcFn func(context.Context, string, interface{}, interface{}) error

func upsertTestCustomizedAuthMethod(
	rpc rpcFn, initialManagementToken string, datacenter string,
	modify func(method *structs.ACLAuthMethod),
) (*structs.ACLAuthMethod, error) {
	name, err := uuid.GenerateUUID()
	if err != nil {
		return nil, err
	}

	req := structs.ACLAuthMethodSetRequest{
		Datacenter: datacenter,
		AuthMethod: structs.ACLAuthMethod{
			Name: "test-method-" + name,
			Type: "testing",
		},
		WriteRequest: structs.WriteRequest{Token: initialManagementToken},
	}

	if modify != nil {
		modify(&req.AuthMethod)
	}

	var out structs.ACLAuthMethod

	err = rpc(context.Background(), "ACL.AuthMethodSet", &req, &out)
	if err != nil {
		return nil, err
	}

	return &out, nil
}

func upsertTestCustomizedBindingRule(rpc rpcFn, initialManagementToken string, datacenter string, modify func(rule *structs.ACLBindingRule)) (*structs.ACLBindingRule, error) {
	req := structs.ACLBindingRuleSetRequest{
		Datacenter:   datacenter,
		BindingRule:  structs.ACLBindingRule{},
		WriteRequest: structs.WriteRequest{Token: initialManagementToken},
	}

	if modify != nil {
		modify(&req.BindingRule)
	}

	var out structs.ACLBindingRule

	err := rpc(context.Background(), "ACL.BindingRuleSet", &req, &out)
	if err != nil {
		return nil, err
	}

	return &out, nil
}

func TestHTTPHandlers_ACLReplicationStatus(t *testing.T) {
	if testing.Short() {
		t.Skip("too slow for testing.Short")
	}

	t.Parallel()
	a := NewTestAgent(t, TestACLConfig())
	defer a.Shutdown()

	req, _ := http.NewRequest("GET", "/v1/acl/replication", nil)
	resp := httptest.NewRecorder()
	testrpc.WaitForLeader(t, a.RPC, "dc1")
	obj, err := a.srv.ACLReplicationStatus(resp, req)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	_, ok := obj.(structs.ACLReplicationStatus)
	if !ok {
		t.Fatalf("should work")
	}
}
