2526 lines
79 KiB
Go
2526 lines
79 KiB
Go
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-memdb"
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/testutil"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestACLEndpoint_GetPolicy(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
policy := mock.ACLPolicy()
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy})
|
|
|
|
anonymousPolicy := mock.ACLPolicy()
|
|
anonymousPolicy.Name = "anonymous"
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1001, []*structs.ACLPolicy{anonymousPolicy})
|
|
|
|
// Create a token with one the policy
|
|
token := mock.ACLToken()
|
|
token.Policies = []string{policy.Name}
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1002, []*structs.ACLToken{token})
|
|
|
|
// Lookup the policy
|
|
get := &structs.ACLPolicySpecificRequest{
|
|
Name: policy.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.SingleACLPolicyResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, policy, resp.Policy)
|
|
|
|
// Lookup non-existing policy
|
|
get.Name = uuid.Generate()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1001), resp.Index)
|
|
assert.Nil(t, resp.Policy)
|
|
|
|
// Lookup the policy with the token
|
|
get = &structs.ACLPolicySpecificRequest{
|
|
Name: policy.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var resp2 structs.SingleACLPolicyResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.EqualValues(t, 1000, resp2.Index)
|
|
assert.Equal(t, policy, resp2.Policy)
|
|
|
|
// Lookup the anonymous policy with no token
|
|
get = &structs.ACLPolicySpecificRequest{
|
|
Name: anonymousPolicy.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var resp3 structs.SingleACLPolicyResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp3); err != nil {
|
|
require.NoError(t, err)
|
|
}
|
|
assert.EqualValues(t, 1001, resp3.Index)
|
|
assert.Equal(t, anonymousPolicy, resp3.Policy)
|
|
|
|
// Lookup non-anonoymous policy with no token
|
|
get = &structs.ACLPolicySpecificRequest{
|
|
Name: policy.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var resp4 structs.SingleACLPolicyResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", get, &resp4)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), structs.ErrPermissionDenied.Error())
|
|
}
|
|
|
|
func TestACLEndpoint_GetPolicy_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the policies
|
|
p1 := mock.ACLPolicy()
|
|
p2 := mock.ACLPolicy()
|
|
|
|
// First create an unrelated policy
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{p1})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Upsert the policy we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 200, []*structs.ACLPolicy{p2})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Lookup the policy
|
|
req := &structs.ACLPolicySpecificRequest{
|
|
Name: p2.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.SingleACLPolicyResponse
|
|
start := time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
if resp.Index != 200 {
|
|
t.Fatalf("Bad index: %d %d", resp.Index, 200)
|
|
}
|
|
if resp.Policy == nil || resp.Policy.Name != p2.Name {
|
|
t.Fatalf("bad: %#v", resp.Policy)
|
|
}
|
|
|
|
// Eval delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.DeleteACLPolicies(structs.MsgTypeTestSetup, 300, []string{p2.Name})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.SingleACLPolicyResponse
|
|
start = time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
if resp2.Index != 300 {
|
|
t.Fatalf("Bad index: %d %d", resp2.Index, 300)
|
|
}
|
|
if resp2.Policy != nil {
|
|
t.Fatalf("bad: %#v", resp2.Policy)
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_GetPolicies(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
policy := mock.ACLPolicy()
|
|
policy2 := mock.ACLPolicy()
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy2})
|
|
|
|
// Lookup the policy
|
|
get := &structs.ACLPolicySetRequest{
|
|
Names: []string{policy.Name, policy2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLPolicySetResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 2, len(resp.Policies))
|
|
assert.Equal(t, policy, resp.Policies[policy.Name])
|
|
assert.Equal(t, policy2, resp.Policies[policy2.Name])
|
|
|
|
// Lookup non-existing policy
|
|
get.Names = []string{uuid.Generate()}
|
|
resp = structs.ACLPolicySetResponse{}
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 0, len(resp.Policies))
|
|
}
|
|
|
|
func TestACLEndpoint_GetPolicies_TokenSubset(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
policy := mock.ACLPolicy()
|
|
policy2 := mock.ACLPolicy()
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy2})
|
|
|
|
token := mock.ACLToken()
|
|
token.Policies = []string{policy.Name}
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{token})
|
|
|
|
// Lookup the policy which is a subset of our tokens
|
|
get := &structs.ACLPolicySetRequest{
|
|
Names: []string{policy.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLPolicySetResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 1, len(resp.Policies))
|
|
assert.Equal(t, policy, resp.Policies[policy.Name])
|
|
|
|
// Lookup non-associated policy
|
|
get.Names = []string{policy2.Name}
|
|
resp = structs.ACLPolicySetResponse{}
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp); err == nil {
|
|
t.Fatalf("expected error")
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_GetPolicies_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the policies
|
|
p1 := mock.ACLPolicy()
|
|
p2 := mock.ACLPolicy()
|
|
|
|
// First create an unrelated policy
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 100, []*structs.ACLPolicy{p1})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Upsert the policy we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 200, []*structs.ACLPolicy{p2})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Lookup the policy
|
|
req := &structs.ACLPolicySetRequest{
|
|
Names: []string{p2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLPolicySetResponse
|
|
start := time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
if resp.Index != 200 {
|
|
t.Fatalf("Bad index: %d %d", resp.Index, 200)
|
|
}
|
|
if len(resp.Policies) == 0 || resp.Policies[p2.Name] == nil {
|
|
t.Fatalf("bad: %#v", resp.Policies)
|
|
}
|
|
|
|
// Eval delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.DeleteACLPolicies(structs.MsgTypeTestSetup, 300, []string{p2.Name})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.ACLPolicySetResponse
|
|
start = time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
if resp2.Index != 300 {
|
|
t.Fatalf("Bad index: %d %d", resp2.Index, 300)
|
|
}
|
|
if len(resp2.Policies) != 0 {
|
|
t.Fatalf("bad: %#v", resp2.Policies)
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_ListPolicies(t *testing.T) {
|
|
ci.Parallel(t)
|
|
assert := assert.New(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLPolicy()
|
|
p2 := mock.ACLPolicy()
|
|
|
|
p1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
|
p2.Name = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9"
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{p1, p2})
|
|
|
|
// Create a token with one of those policies
|
|
token := mock.ACLToken()
|
|
token.Policies = []string{p1.Name}
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1001, []*structs.ACLToken{token})
|
|
|
|
// Lookup the policies
|
|
get := &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLPolicyListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.EqualValues(1000, resp.Index)
|
|
assert.Len(resp.Policies, 2)
|
|
|
|
// Lookup the policies by prefix
|
|
get = &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Prefix: "aaaabb",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp2 structs.ACLPolicyListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.EqualValues(1000, resp2.Index)
|
|
assert.Len(resp2.Policies, 1)
|
|
|
|
// List policies using the created token
|
|
get = &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var resp3 structs.ACLPolicyListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp3); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.EqualValues(1000, resp3.Index)
|
|
if assert.Len(resp3.Policies, 1) {
|
|
assert.Equal(resp3.Policies[0].Name, p1.Name)
|
|
}
|
|
}
|
|
|
|
// TestACLEndpoint_ListPolicies_Unauthenticated asserts that
|
|
// unauthenticated ListPolicies returns anonymous policy if one
|
|
// exists, otherwise, empty
|
|
func TestACLEndpoint_ListPolicies_Unauthenticated(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, _, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
listPolicies := func() (*structs.ACLPolicyListResponse, error) {
|
|
// Lookup the policies
|
|
get := &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
|
|
var resp structs.ACLPolicyListResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", get, &resp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
p1 := mock.ACLPolicy()
|
|
p1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{p1})
|
|
|
|
t.Run("no anonymous policy", func(t *testing.T) {
|
|
resp, err := listPolicies()
|
|
require.NoError(t, err)
|
|
require.Empty(t, resp.Policies)
|
|
require.Equal(t, uint64(1000), resp.Index)
|
|
})
|
|
|
|
// now try with anonymous policy
|
|
p2 := mock.ACLPolicy()
|
|
p2.Name = "anonymous"
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1001, []*structs.ACLPolicy{p2})
|
|
|
|
t.Run("with anonymous policy", func(t *testing.T) {
|
|
resp, err := listPolicies()
|
|
require.NoError(t, err)
|
|
require.Len(t, resp.Policies, 1)
|
|
require.Equal(t, "anonymous", resp.Policies[0].Name)
|
|
require.Equal(t, uint64(1001), resp.Index)
|
|
})
|
|
}
|
|
|
|
func TestACLEndpoint_ListPolicies_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the policy
|
|
policy := mock.ACLPolicy()
|
|
|
|
// Upsert eval triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
if err := state.UpsertACLPolicies(structs.MsgTypeTestSetup, 2, []*structs.ACLPolicy{policy}); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req := &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 1,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
start := time.Now()
|
|
var resp structs.ACLPolicyListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
assert.Equal(t, uint64(2), resp.Index)
|
|
if len(resp.Policies) != 1 || resp.Policies[0].Name != policy.Name {
|
|
t.Fatalf("bad: %#v", resp.Policies)
|
|
}
|
|
|
|
// Eval deletion triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
if err := state.DeleteACLPolicies(structs.MsgTypeTestSetup, 3, []string{policy.Name}); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.MinQueryIndex = 2
|
|
start = time.Now()
|
|
var resp2 structs.ACLPolicyListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
assert.Equal(t, uint64(3), resp2.Index)
|
|
assert.Equal(t, 0, len(resp2.Policies))
|
|
}
|
|
|
|
func TestACLEndpoint_DeletePolicies(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLPolicy()
|
|
s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{p1})
|
|
|
|
// Lookup the policies
|
|
req := &structs.ACLPolicyDeleteRequest{
|
|
Names: []string{p1.Name},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.GenericResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.DeletePolicies", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
}
|
|
|
|
func TestACLEndpoint_UpsertPolicies(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLPolicy()
|
|
|
|
// Lookup the policies
|
|
req := &structs.ACLPolicyUpsertRequest{
|
|
Policies: []*structs.ACLPolicy{p1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.GenericResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.UpsertPolicies", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
|
|
// Check we created the policy
|
|
out, err := s1.fsm.State().ACLPolicyByName(nil, p1.Name)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, out)
|
|
}
|
|
|
|
func TestACLEndpoint_UpsertPolicies_Invalid(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLPolicy()
|
|
p1.Rules = "blah blah invalid"
|
|
|
|
// Lookup the policies
|
|
req := &structs.ACLPolicyUpsertRequest{
|
|
Policies: []*structs.ACLPolicy{p1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.GenericResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.UpsertPolicies", req, &resp)
|
|
assert.NotNil(t, err)
|
|
if !strings.Contains(err.Error(), "failed to parse") {
|
|
t.Fatalf("bad: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_GetToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
token := mock.ACLToken()
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{token})
|
|
|
|
// Lookup the token
|
|
get := &structs.ACLTokenSpecificRequest{
|
|
AccessorID: token.AccessorID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.SingleACLTokenResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetToken", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, token, resp.Token)
|
|
|
|
// Lookup non-existing token
|
|
get.AccessorID = uuid.Generate()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetToken", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Nil(t, resp.Token)
|
|
|
|
// Lookup the token by accessor id using the tokens secret ID
|
|
get.AccessorID = token.AccessorID
|
|
get.AuthToken = token.SecretID
|
|
var resp2 structs.SingleACLTokenResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetToken", get, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp2.Index)
|
|
assert.Equal(t, token, resp2.Token)
|
|
}
|
|
|
|
func TestACLEndpoint_GetToken_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the tokens
|
|
p1 := mock.ACLToken()
|
|
p2 := mock.ACLToken()
|
|
|
|
// First create an unrelated token
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 100, []*structs.ACLToken{p1})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Upsert the token we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 200, []*structs.ACLToken{p2})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Lookup the token
|
|
req := &structs.ACLTokenSpecificRequest{
|
|
AccessorID: p2.AccessorID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.SingleACLTokenResponse
|
|
start := time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetToken", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
if resp.Index != 200 {
|
|
t.Fatalf("Bad index: %d %d", resp.Index, 200)
|
|
}
|
|
if resp.Token == nil || resp.Token.AccessorID != p2.AccessorID {
|
|
t.Fatalf("bad: %#v", resp.Token)
|
|
}
|
|
|
|
// Eval delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.DeleteACLTokens(structs.MsgTypeTestSetup, 300, []string{p2.AccessorID})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.SingleACLTokenResponse
|
|
start = time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetToken", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
if resp2.Index != 300 {
|
|
t.Fatalf("Bad index: %d %d", resp2.Index, 300)
|
|
}
|
|
if resp2.Token != nil {
|
|
t.Fatalf("bad: %#v", resp2.Token)
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_GetTokens(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
token := mock.ACLToken()
|
|
token2 := mock.ACLToken()
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{token, token2})
|
|
|
|
// Lookup the token
|
|
get := &structs.ACLTokenSetRequest{
|
|
AccessorIDS: []string{token.AccessorID, token2.AccessorID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLTokenSetResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetTokens", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 2, len(resp.Tokens))
|
|
assert.Equal(t, token, resp.Tokens[token.AccessorID])
|
|
|
|
// Lookup non-existing token
|
|
get.AccessorIDS = []string{uuid.Generate()}
|
|
resp = structs.ACLTokenSetResponse{}
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetTokens", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 0, len(resp.Tokens))
|
|
}
|
|
|
|
func TestACLEndpoint_GetTokens_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the tokens
|
|
p1 := mock.ACLToken()
|
|
p2 := mock.ACLToken()
|
|
|
|
// First create an unrelated token
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 100, []*structs.ACLToken{p1})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Upsert the token we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 200, []*structs.ACLToken{p2})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
// Lookup the token
|
|
req := &structs.ACLTokenSetRequest{
|
|
AccessorIDS: []string{p2.AccessorID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLTokenSetResponse
|
|
start := time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetTokens", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
if resp.Index != 200 {
|
|
t.Fatalf("Bad index: %d %d", resp.Index, 200)
|
|
}
|
|
if len(resp.Tokens) == 0 || resp.Tokens[p2.AccessorID] == nil {
|
|
t.Fatalf("bad: %#v", resp.Tokens)
|
|
}
|
|
|
|
// Eval delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
err := state.DeleteACLTokens(structs.MsgTypeTestSetup, 300, []string{p2.AccessorID})
|
|
if err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.ACLTokenSetResponse
|
|
start = time.Now()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.GetTokens", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
if resp2.Index != 300 {
|
|
t.Fatalf("Bad index: %d %d", resp2.Index, 300)
|
|
}
|
|
if len(resp2.Tokens) != 0 {
|
|
t.Fatalf("bad: %#v", resp2.Tokens)
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_ListTokens(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLToken()
|
|
p2 := mock.ACLToken()
|
|
p2.Global = true
|
|
|
|
p1.AccessorID = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
|
p2.AccessorID = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9"
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{p1, p2})
|
|
|
|
// Lookup the tokens
|
|
get := &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLTokenListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, 3, len(resp.Tokens))
|
|
|
|
// Lookup the tokens by prefix
|
|
get = &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Prefix: "aaaabb",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp2 structs.ACLTokenListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", get, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp2.Index)
|
|
assert.Equal(t, 1, len(resp2.Tokens))
|
|
|
|
// Lookup the global tokens
|
|
get = &structs.ACLTokenListRequest{
|
|
GlobalOnly: true,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp3 structs.ACLTokenListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", get, &resp3); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp3.Index)
|
|
assert.Equal(t, 2, len(resp3.Tokens))
|
|
}
|
|
|
|
func TestACLEndpoint_ListTokens_PaginationFiltering(t *testing.T) {
|
|
ci.Parallel(t)
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// create a set of ACL tokens. these are in the order that the state store
|
|
// will return them from the iterator (sorted by key) for ease of writing
|
|
// tests
|
|
mocks := []struct {
|
|
ids []string
|
|
typ string
|
|
}{
|
|
{ids: []string{"aaaa1111-3350-4b4b-d185-0e1992ed43e9"}, typ: "management"}, // 0
|
|
{ids: []string{"aaaaaa22-3350-4b4b-d185-0e1992ed43e9"}}, // 1
|
|
{ids: []string{"aaaaaa33-3350-4b4b-d185-0e1992ed43e9"}}, // 2
|
|
{ids: []string{"aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"}}, // 3
|
|
{ids: []string{"aaaaaabb-3350-4b4b-d185-0e1992ed43e9"}}, // 4
|
|
{ids: []string{"aaaaaacc-3350-4b4b-d185-0e1992ed43e9"}}, // 5
|
|
{ids: []string{"aaaaaadd-3350-4b4b-d185-0e1992ed43e9"}}, // 6
|
|
{ids: []string{"00000111-3350-4b4b-d185-0e1992ed43e9"}}, // 7
|
|
{ids: []string{ // 8
|
|
"00000222-3350-4b4b-d185-0e1992ed43e9",
|
|
"00000333-3350-4b4b-d185-0e1992ed43e9",
|
|
}},
|
|
{}, // 9, index missing
|
|
{ids: []string{"bbbb1111-3350-4b4b-d185-0e1992ed43e9"}}, // 10
|
|
}
|
|
|
|
state := s1.fsm.State()
|
|
|
|
var bootstrapToken string
|
|
for i, m := range mocks {
|
|
tokensInTx := []*structs.ACLToken{}
|
|
for _, id := range m.ids {
|
|
token := mock.ACLToken()
|
|
token.AccessorID = id
|
|
token.Type = m.typ
|
|
tokensInTx = append(tokensInTx, token)
|
|
}
|
|
index := 1000 + uint64(i)
|
|
|
|
// bootstrap cluster with the first token
|
|
if i == 0 {
|
|
token := tokensInTx[0]
|
|
bootstrapToken = token.SecretID
|
|
err := s1.State().BootstrapACLTokens(structs.MsgTypeTestSetup, index, 0, token)
|
|
require.NoError(t, err)
|
|
|
|
err = state.UpsertACLTokens(structs.MsgTypeTestSetup, index, tokensInTx[1:])
|
|
require.NoError(t, err)
|
|
} else {
|
|
err := state.UpsertACLTokens(structs.MsgTypeTestSetup, index, tokensInTx)
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
cases := []struct {
|
|
name string
|
|
prefix string
|
|
filter string
|
|
nextToken string
|
|
pageSize int32
|
|
expectedNextToken string
|
|
expectedIDs []string
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "test01 size-2 page-1",
|
|
pageSize: 2,
|
|
expectedNextToken: "1002.aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"aaaa1111-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaa22-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test02 size-2 page-1 with prefix",
|
|
prefix: "aaaa",
|
|
pageSize: 2,
|
|
expectedNextToken: "aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"aaaa1111-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaa22-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test03 size-2 page-2 default NS",
|
|
pageSize: 2,
|
|
nextToken: "1002.aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedNextToken: "1004.aaaaaabb-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaaaa-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test04 go-bexpr filter",
|
|
filter: `AccessorID matches "^a+[123]"`,
|
|
expectedIDs: []string{
|
|
"aaaa1111-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaa22-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test05 go-bexpr filter with pagination",
|
|
filter: `AccessorID matches "^a+[123]"`,
|
|
pageSize: 2,
|
|
expectedNextToken: "1002.aaaaaa33-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"aaaa1111-3350-4b4b-d185-0e1992ed43e9",
|
|
"aaaaaa22-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test06 go-bexpr invalid expression",
|
|
filter: `NotValid`,
|
|
expectedError: "failed to read filter expression",
|
|
},
|
|
{
|
|
name: "test07 go-bexpr invalid field",
|
|
filter: `InvalidField == "value"`,
|
|
expectedError: "error finding value in datum",
|
|
},
|
|
{
|
|
name: "test08 non-lexicographic order",
|
|
pageSize: 1,
|
|
nextToken: "1007.00000111-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedNextToken: "1008.00000222-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"00000111-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test09 same index",
|
|
pageSize: 1,
|
|
nextToken: "1008.00000222-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedNextToken: "1008.00000333-3350-4b4b-d185-0e1992ed43e9",
|
|
expectedIDs: []string{
|
|
"00000222-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
{
|
|
name: "test10 missing index",
|
|
pageSize: 1,
|
|
nextToken: "1009.e9522802-0cd8-4b1d-9c9e-ab3d97938371",
|
|
expectedIDs: []string{
|
|
"bbbb1111-3350-4b4b-d185-0e1992ed43e9",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Prefix: tc.prefix,
|
|
Filter: tc.filter,
|
|
PerPage: tc.pageSize,
|
|
NextToken: tc.nextToken,
|
|
},
|
|
}
|
|
req.AuthToken = bootstrapToken
|
|
var resp structs.ACLTokenListResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", req, &resp)
|
|
if tc.expectedError == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), tc.expectedError)
|
|
return
|
|
}
|
|
|
|
gotIDs := []string{}
|
|
for _, token := range resp.Tokens {
|
|
gotIDs = append(gotIDs, token.AccessorID)
|
|
}
|
|
require.Equal(t, tc.expectedIDs, gotIDs, "unexpected page of tokens")
|
|
require.Equal(t, tc.expectedNextToken, resp.QueryMeta.NextToken, "unexpected NextToken")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_ListTokens_Order(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create register requests
|
|
uuid1 := uuid.Generate()
|
|
token1 := mock.ACLManagementToken()
|
|
token1.AccessorID = uuid1
|
|
|
|
uuid2 := uuid.Generate()
|
|
token2 := mock.ACLToken()
|
|
token2.AccessorID = uuid2
|
|
|
|
uuid3 := uuid.Generate()
|
|
token3 := mock.ACLToken()
|
|
token3.AccessorID = uuid3
|
|
|
|
// bootstrap cluster with the first token
|
|
bootstrapToken := token1.SecretID
|
|
err := s1.State().BootstrapACLTokens(structs.MsgTypeTestSetup, 1000, 0, token1)
|
|
require.NoError(t, err)
|
|
|
|
err = s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1001, []*structs.ACLToken{token2})
|
|
require.NoError(t, err)
|
|
|
|
err = s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1002, []*structs.ACLToken{token3})
|
|
require.NoError(t, err)
|
|
|
|
// update token2 again so we can later assert create index order did not change
|
|
err = s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1003, []*structs.ACLToken{token2})
|
|
require.NoError(t, err)
|
|
|
|
t.Run("default", func(t *testing.T) {
|
|
// Lookup the tokens in the default order (oldest first)
|
|
get := &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
},
|
|
}
|
|
get.AuthToken = bootstrapToken
|
|
|
|
var resp structs.ACLTokenListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", get, &resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(1003), resp.Index)
|
|
require.Len(t, resp.Tokens, 3)
|
|
|
|
// Assert returned order is by CreateIndex (ascending)
|
|
require.Equal(t, uint64(1000), resp.Tokens[0].CreateIndex)
|
|
require.Equal(t, uuid1, resp.Tokens[0].AccessorID)
|
|
|
|
require.Equal(t, uint64(1001), resp.Tokens[1].CreateIndex)
|
|
require.Equal(t, uuid2, resp.Tokens[1].AccessorID)
|
|
|
|
require.Equal(t, uint64(1002), resp.Tokens[2].CreateIndex)
|
|
require.Equal(t, uuid3, resp.Tokens[2].AccessorID)
|
|
})
|
|
|
|
t.Run("reverse", func(t *testing.T) {
|
|
// Lookup the tokens in reverse order (newest first)
|
|
get := &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
Reverse: true,
|
|
},
|
|
}
|
|
get.AuthToken = bootstrapToken
|
|
|
|
var resp structs.ACLTokenListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", get, &resp)
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(1003), resp.Index)
|
|
require.Len(t, resp.Tokens, 3)
|
|
|
|
// Assert returned order is by CreateIndex (descending)
|
|
require.Equal(t, uint64(1002), resp.Tokens[0].CreateIndex)
|
|
require.Equal(t, uuid3, resp.Tokens[0].AccessorID)
|
|
|
|
require.Equal(t, uint64(1001), resp.Tokens[1].CreateIndex)
|
|
require.Equal(t, uuid2, resp.Tokens[1].AccessorID)
|
|
|
|
require.Equal(t, uint64(1000), resp.Tokens[2].CreateIndex)
|
|
require.Equal(t, uuid1, resp.Tokens[2].AccessorID)
|
|
})
|
|
}
|
|
|
|
func TestACLEndpoint_ListTokens_Blocking(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
state := s1.fsm.State()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the token
|
|
token := mock.ACLToken()
|
|
|
|
// Upsert eval triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
if err := state.UpsertACLTokens(structs.MsgTypeTestSetup, 3, []*structs.ACLToken{token}); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req := &structs.ACLTokenListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 2,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
start := time.Now()
|
|
var resp structs.ACLTokenListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
assert.Equal(t, uint64(3), resp.Index)
|
|
if len(resp.Tokens) != 2 {
|
|
t.Fatalf("bad: %#v", resp.Tokens)
|
|
}
|
|
|
|
// Eval deletion triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
if err := state.DeleteACLTokens(structs.MsgTypeTestSetup, 4, []string{token.AccessorID}); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
})
|
|
|
|
req.MinQueryIndex = 3
|
|
start = time.Now()
|
|
var resp2 structs.ACLTokenListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ListTokens", req, &resp2); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
assert.Equal(t, uint64(4), resp2.Index)
|
|
assert.Equal(t, 1, len(resp2.Tokens))
|
|
}
|
|
|
|
func TestACLEndpoint_DeleteTokens(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
p1 := mock.ACLToken()
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{p1})
|
|
|
|
// Lookup the tokens
|
|
req := &structs.ACLTokenDeleteRequest{
|
|
AccessorIDs: []string{p1.AccessorID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.GenericResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.DeleteTokens", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
}
|
|
|
|
func TestACLEndpoint_DeleteTokens_WithNonexistentToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
assert := assert.New(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
nonexistentToken := mock.ACLToken()
|
|
|
|
// Lookup the policies
|
|
req := &structs.ACLTokenDeleteRequest{
|
|
AccessorIDs: []string{nonexistentToken.AccessorID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.GenericResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.DeleteTokens", req, &resp)
|
|
|
|
assert.NotNil(err)
|
|
expectedError := fmt.Sprintf("Cannot delete nonexistent tokens: %s", nonexistentToken.AccessorID)
|
|
assert.Contains(err.Error(), expectedError)
|
|
}
|
|
|
|
func TestACLEndpoint_Bootstrap(t *testing.T) {
|
|
ci.Parallel(t)
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Lookup the tokens
|
|
req := &structs.ACLTokenBootstrapRequest{
|
|
WriteRequest: structs.WriteRequest{Region: "global"},
|
|
}
|
|
var resp structs.ACLTokenUpsertResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
assert.NotNil(t, resp.Tokens[0])
|
|
|
|
// Get the token out from the response
|
|
created := resp.Tokens[0]
|
|
assert.NotEqual(t, "", created.AccessorID)
|
|
assert.NotEqual(t, "", created.SecretID)
|
|
assert.NotEqual(t, time.Time{}, created.CreateTime)
|
|
assert.Equal(t, structs.ACLManagementToken, created.Type)
|
|
assert.Equal(t, "Bootstrap Token", created.Name)
|
|
assert.Equal(t, true, created.Global)
|
|
|
|
// Check we created the token
|
|
out, err := s1.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, created, out)
|
|
}
|
|
|
|
func TestACLEndpoint_BootstrapOperator(t *testing.T) {
|
|
ci.Parallel(t)
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.ACLEnabled = true
|
|
})
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Lookup the tokens
|
|
req := &structs.ACLTokenBootstrapRequest{
|
|
WriteRequest: structs.WriteRequest{Region: "global"},
|
|
BootstrapSecret: "2b778dd9-f5f1-6f29-b4b4-9a5fa948757a",
|
|
}
|
|
var resp structs.ACLTokenUpsertResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
assert.NotNil(t, resp.Tokens[0])
|
|
|
|
// Get the token out from the response
|
|
created := resp.Tokens[0]
|
|
assert.NotEqual(t, "", created.AccessorID)
|
|
assert.NotEqual(t, "", created.SecretID)
|
|
assert.NotEqual(t, time.Time{}, created.CreateTime)
|
|
assert.Equal(t, structs.ACLManagementToken, created.Type)
|
|
assert.Equal(t, "Bootstrap Token", created.Name)
|
|
assert.Equal(t, true, created.Global)
|
|
|
|
// Check we created the token
|
|
out, err := s1.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, created, out)
|
|
// Check we have the correct operator token
|
|
tokenout, err := s1.fsm.State().ACLTokenBySecretID(nil, created.SecretID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, created, tokenout)
|
|
}
|
|
|
|
func TestACLEndpoint_Bootstrap_Reset(t *testing.T) {
|
|
ci.Parallel(t)
|
|
dir := t.TempDir()
|
|
s1, cleanupS1 := TestServer(t, func(c *Config) {
|
|
c.ACLEnabled = true
|
|
c.DataDir = dir
|
|
c.DevMode = false
|
|
})
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Lookup the tokens
|
|
req := &structs.ACLTokenBootstrapRequest{
|
|
WriteRequest: structs.WriteRequest{Region: "global"},
|
|
}
|
|
var resp structs.ACLTokenUpsertResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
assert.NotNil(t, resp.Tokens[0])
|
|
resetIdx := resp.Tokens[0].CreateIndex
|
|
|
|
// Try again, should fail
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err == nil {
|
|
t.Fatalf("expected err")
|
|
}
|
|
|
|
// Create the reset file
|
|
output := []byte(fmt.Sprintf("%d", resetIdx))
|
|
path := filepath.Join(dir, aclBootstrapReset)
|
|
assert.Nil(t, ioutil.WriteFile(path, output, 0755))
|
|
|
|
// Try again, should work with reset
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.NotEqual(t, uint64(0), resp.Index)
|
|
assert.NotNil(t, resp.Tokens[0])
|
|
|
|
// Get the token out from the response
|
|
created := resp.Tokens[0]
|
|
assert.NotEqual(t, "", created.AccessorID)
|
|
assert.NotEqual(t, "", created.SecretID)
|
|
assert.NotEqual(t, time.Time{}, created.CreateTime)
|
|
assert.Equal(t, structs.ACLManagementToken, created.Type)
|
|
assert.Equal(t, "Bootstrap Token", created.Name)
|
|
assert.Equal(t, true, created.Global)
|
|
|
|
// Check we created the token
|
|
out, err := s1.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, created, out)
|
|
|
|
// Try again, should fail
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.Bootstrap", req, &resp); err == nil {
|
|
t.Fatalf("expected err")
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_UpsertTokens(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Each sub-test uses the same server to avoid creating a new one for each
|
|
// test. This means some care has to be taken with resource naming, but
|
|
// does avoid lots of calls to systems such as freeport.
|
|
testServer, rootACLToken, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
testFn func(testServer *Server, aclToken *structs.ACLToken)
|
|
}{
|
|
{
|
|
name: "valid client token",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Create the register request with a mocked token. We must set
|
|
// an empty accessorID, otherwise Nomad treats this as an
|
|
// update request.
|
|
p1 := mock.ACLToken()
|
|
p1.AccessorID = ""
|
|
|
|
req := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{p1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLTokenUpsertResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, req, &resp))
|
|
must.Greater(t, resp.Index, 0)
|
|
|
|
// Get the token out from the response.
|
|
created := resp.Tokens[0]
|
|
require.NotEqual(t, "", created.AccessorID)
|
|
require.NotEqual(t, "", created.SecretID)
|
|
require.NotEqual(t, time.Time{}, created.CreateTime)
|
|
require.Equal(t, p1.Type, created.Type)
|
|
require.Equal(t, p1.Policies, created.Policies)
|
|
require.Equal(t, p1.Name, created.Name)
|
|
|
|
// Check we created the token.
|
|
out, err := testServer.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
|
|
require.Nil(t, err)
|
|
require.Equal(t, created, out)
|
|
|
|
// Update the token type and policy list so we can try updating
|
|
// it.
|
|
req.Tokens[0] = created
|
|
created.Type = "management"
|
|
created.Policies = nil
|
|
|
|
// Track the first upsert index, so we can test the next
|
|
// response against this and perform the update.
|
|
originalIndex := resp.Index
|
|
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, req, &resp))
|
|
require.Greater(t, resp.Index, originalIndex)
|
|
|
|
// Read the token from state and perform an equality check to
|
|
// ensure everything matches as we expect.
|
|
out, err = testServer.fsm.State().ACLTokenByAccessorID(nil, created.AccessorID)
|
|
require.Nil(t, err)
|
|
require.Equal(t, created, out)
|
|
},
|
|
},
|
|
{
|
|
name: "valid management token with expiration",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Create our RPC request object which includes a management
|
|
// token with a TTL.
|
|
req := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-management-token-" + uuid.Generate(),
|
|
Type: structs.ACLManagementToken,
|
|
ExpirationTTL: 10 * time.Minute,
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the expiration time is as
|
|
// expected.
|
|
var resp structs.ACLTokenUpsertResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, req, &resp))
|
|
require.Equal(t, 10*time.Minute, resp.Tokens[0].ExpirationTime.Sub(resp.Tokens[0].CreateTime))
|
|
},
|
|
},
|
|
{
|
|
name: "valid client token with expiration",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Create an ACL policy so this can be associated to our client
|
|
// token.
|
|
policyReq := &structs.ACLPolicyUpsertRequest{
|
|
Policies: []*structs.ACLPolicy{mock.ACLPolicy()},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
var policyResp structs.GenericResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertPoliciesRPCMethod, policyReq, &policyResp))
|
|
|
|
// Create our RPC request object which includes a client token
|
|
// with a TTL that is associated to policies above.
|
|
tokenReq := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-client-token-" + uuid.Generate(),
|
|
Type: structs.ACLClientToken,
|
|
Policies: []string{policyReq.Policies[0].Name},
|
|
ExpirationTTL: 10 * time.Minute,
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the expiration time is as
|
|
// expected.
|
|
var tokenResp structs.ACLTokenUpsertResponse
|
|
require.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, tokenReq, &tokenResp))
|
|
require.Equal(t, 10*time.Minute, tokenResp.Tokens[0].ExpirationTime.Sub(tokenResp.Tokens[0].CreateTime))
|
|
},
|
|
},
|
|
{
|
|
name: "invalid token type",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Create our RPC request object which includes a token with an
|
|
// unknown type. This allows us to ensure the RPC handler calls
|
|
// the validation func.
|
|
tokenReq := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-blah-token-" + uuid.Generate(),
|
|
Type: "blah",
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the expiration time is as
|
|
// expected.
|
|
var tokenResp structs.ACLTokenUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, tokenReq, &tokenResp)
|
|
require.ErrorContains(t, err, "token type must be client or management")
|
|
require.Empty(t, tokenResp.Tokens)
|
|
},
|
|
},
|
|
{
|
|
name: "token with role links",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Attempt to create a token with a link to a role that does
|
|
// not exist in state.
|
|
tokenReq1 := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-lovely-token-" + uuid.Generate(),
|
|
Type: structs.ACLClientToken,
|
|
Roles: []*structs.ACLTokenRoleLink{{Name: "cant-find-me"}},
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the expiration time is as
|
|
// expected.
|
|
var tokenResp1 structs.ACLTokenUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, tokenReq1, &tokenResp1)
|
|
require.ErrorContains(t, err, "cannot find role cant-find-me")
|
|
require.Empty(t, tokenResp1.Tokens)
|
|
|
|
// Create an ACL policy that will be linked from an ACL role
|
|
// and enter this into state.
|
|
policy1 := mock.ACLPolicy()
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1}))
|
|
|
|
// Create an ACL role that links to the above policy.
|
|
aclRole1 := mock.ACLRole()
|
|
aclRole1.Policies = []*structs.ACLRolePolicyLink{{Name: policy1.Name}}
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{aclRole1}, false))
|
|
|
|
// Create a token which references the created ACL role. This
|
|
// role reference is duplicated to ensure the handler
|
|
// de-duplicates this before putting it into state.
|
|
// not exist in state.
|
|
tokenReq2 := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-lovely-token-" + uuid.Generate(),
|
|
Type: structs.ACLClientToken,
|
|
Roles: []*structs.ACLTokenRoleLink{
|
|
{ID: aclRole1.ID},
|
|
{ID: aclRole1.ID},
|
|
{ID: aclRole1.ID},
|
|
},
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the returned token is as
|
|
// expected.
|
|
var tokenResp2 structs.ACLTokenUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, tokenReq2, &tokenResp2)
|
|
require.NoError(t, err)
|
|
require.Len(t, tokenResp2.Tokens, 1)
|
|
require.Len(t, tokenResp2.Tokens[0].Policies, 0)
|
|
require.Len(t, tokenResp2.Tokens[0].Roles, 1)
|
|
require.Equal(t, []*structs.ACLTokenRoleLink{{
|
|
ID: aclRole1.ID, Name: aclRole1.Name}}, tokenResp2.Tokens[0].Roles)
|
|
},
|
|
},
|
|
{
|
|
name: "token with role and policy links",
|
|
testFn: func(testServer *Server, aclToken *structs.ACLToken) {
|
|
|
|
// Create two ACL policies that will be used for ACL role and
|
|
// policy linking.
|
|
policy1 := mock.ACLPolicy()
|
|
policy2 := mock.ACLPolicy()
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create an ACL role that links to one of the above policies.
|
|
aclRole1 := mock.ACLRole()
|
|
aclRole1.Policies = []*structs.ACLRolePolicyLink{{Name: policy1.Name}}
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{aclRole1}, false))
|
|
|
|
// Create an ACL token with both ACL role and policy links.
|
|
tokenReq1 := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{
|
|
{
|
|
Name: "my-lovely-token-" + uuid.Generate(),
|
|
Type: structs.ACLClientToken,
|
|
Policies: []string{policy2.Name},
|
|
Roles: []*structs.ACLTokenRoleLink{{ID: aclRole1.ID}},
|
|
},
|
|
},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
|
|
// Send the RPC request and ensure the returned token has
|
|
// policy and ACL role links as expected.
|
|
var tokenResp1 structs.ACLTokenUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertTokensRPCMethod, tokenReq1, &tokenResp1)
|
|
require.NoError(t, err)
|
|
require.Len(t, tokenResp1.Tokens, 1)
|
|
require.Len(t, tokenResp1.Tokens[0].Policies, 1)
|
|
require.Len(t, tokenResp1.Tokens[0].Roles, 1)
|
|
require.Equal(t, policy2.Name, tokenResp1.Tokens[0].Policies[0])
|
|
require.Equal(t, []*structs.ACLTokenRoleLink{{
|
|
ID: aclRole1.ID, Name: aclRole1.Name}}, tokenResp1.Tokens[0].Roles)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.testFn(testServer, rootACLToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestACLEndpoint_ResolveToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
s1, _, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// Create the register request
|
|
token := mock.ACLToken()
|
|
s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1000, []*structs.ACLToken{token})
|
|
|
|
// Lookup the token
|
|
get := &structs.ResolveACLTokenRequest{
|
|
SecretID: token.SecretID,
|
|
QueryOptions: structs.QueryOptions{Region: "global"},
|
|
}
|
|
var resp structs.ResolveACLTokenResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ResolveToken", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Equal(t, token, resp.Token)
|
|
|
|
// Lookup non-existing token
|
|
get.SecretID = uuid.Generate()
|
|
if err := msgpackrpc.CallWithCodec(codec, "ACL.ResolveToken", get, &resp); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
assert.Equal(t, uint64(1000), resp.Index)
|
|
assert.Nil(t, resp.Token)
|
|
}
|
|
|
|
func TestACLEndpoint_OneTimeToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
// create an ACL token
|
|
|
|
p1 := mock.ACLToken()
|
|
p1.AccessorID = "" // has to be blank to create
|
|
aclReq := &structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{p1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var aclResp structs.ACLTokenUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, "ACL.UpsertTokens", aclReq, &aclResp)
|
|
require.NoError(t, err)
|
|
aclToken := aclResp.Tokens[0]
|
|
|
|
// Generate a one-time token for this ACL token
|
|
upReq := &structs.OneTimeTokenUpsertRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: aclToken.SecretID,
|
|
}}
|
|
|
|
var upResp structs.OneTimeTokenUpsertResponse
|
|
|
|
// Call the upsert RPC
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.UpsertOneTimeToken", upReq, &upResp)
|
|
require.NoError(t, err)
|
|
result := upResp.OneTimeToken
|
|
require.True(t, time.Now().Before(result.ExpiresAt))
|
|
require.Equal(t, aclToken.AccessorID, result.AccessorID)
|
|
|
|
// make sure we can get it back out
|
|
ott, err := s1.fsm.State().OneTimeTokenBySecret(nil, result.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ott)
|
|
|
|
exReq := &structs.OneTimeTokenExchangeRequest{
|
|
OneTimeSecretID: result.OneTimeSecretID,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global", // note: not authenticated!
|
|
}}
|
|
var exResp structs.OneTimeTokenExchangeResponse
|
|
|
|
// Call the exchange RPC
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ExchangeOneTimeToken", exReq, &exResp)
|
|
require.NoError(t, err)
|
|
token := exResp.Token
|
|
require.Equal(t, aclToken.AccessorID, token.AccessorID)
|
|
require.Equal(t, aclToken.SecretID, token.SecretID)
|
|
|
|
// Make sure the one-time token is gone
|
|
ott, err = s1.fsm.State().OneTimeTokenBySecret(nil, result.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, ott)
|
|
|
|
// directly write the OTT to the state store so that we can write an
|
|
// expired OTT, and query to ensure it's been written
|
|
index := exResp.Index
|
|
index += 10
|
|
ott = &structs.OneTimeToken{
|
|
OneTimeSecretID: uuid.Generate(),
|
|
AccessorID: token.AccessorID,
|
|
ExpiresAt: time.Now().Add(-1 * time.Minute),
|
|
}
|
|
|
|
err = s1.fsm.State().UpsertOneTimeToken(structs.MsgTypeTestSetup, index, ott)
|
|
require.NoError(t, err)
|
|
ott, err = s1.fsm.State().OneTimeTokenBySecret(nil, ott.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ott)
|
|
|
|
// Call the exchange RPC; we should not get an exchange for an expired
|
|
// token
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ExchangeOneTimeToken", exReq, &exResp)
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
|
|
|
// expired token should be left in place (until GC comes along)
|
|
ott, err = s1.fsm.State().OneTimeTokenBySecret(nil, ott.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ott)
|
|
|
|
// Call the delete RPC, should fail without proper auth
|
|
expReq := &structs.OneTimeTokenExpireRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ExpireOneTimeTokens",
|
|
expReq, &structs.GenericResponse{})
|
|
require.EqualError(t, err, structs.ErrPermissionDenied.Error(),
|
|
"one-time token garbage collection requires management ACL")
|
|
|
|
// should not have caused an expiration either!
|
|
ott, err = s1.fsm.State().OneTimeTokenBySecret(nil, ott.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, ott)
|
|
|
|
// Call with correct permissions
|
|
expReq.WriteRequest.AuthToken = root.SecretID
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.ExpireOneTimeTokens",
|
|
expReq, &structs.GenericResponse{})
|
|
require.NoError(t, err)
|
|
|
|
// Now the expired OTT should be gone
|
|
ott, err = s1.fsm.State().OneTimeTokenBySecret(nil, result.OneTimeSecretID)
|
|
require.NoError(t, err)
|
|
require.Nil(t, ott)
|
|
}
|
|
|
|
func TestACL_UpsertRoles(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create a mock ACL role and remove the ID so this looks like a creation.
|
|
aclRole1 := mock.ACLRole()
|
|
aclRole1.ID = ""
|
|
|
|
// Attempt to upsert this role without setting an ACL token. This should
|
|
// fail.
|
|
aclRoleReq1 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRolesUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
|
|
// Attempt to upsert this role again, this time setting the ACL root token.
|
|
// This should fail because the linked policies do not exist within state.
|
|
aclRoleReq2 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRolesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.ErrorContains(t, err, "cannot find policy")
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Try the upsert a third time, which should succeed.
|
|
aclRoleReq3 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRolesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp3.ACLRoles, 1)
|
|
require.True(t, aclRole1.Equals(aclRoleResp3.ACLRoles[0]))
|
|
|
|
// Perform an update of the ACL role by removing a policy and changing the
|
|
// name.
|
|
aclRole1Copy := aclRole1.Copy()
|
|
aclRole1Copy.Name = "updated-role-name"
|
|
aclRole1Copy.Policies = append(aclRole1Copy.Policies[:1], aclRole1Copy.Policies[1+1:]...)
|
|
aclRole1Copy.SetHash()
|
|
|
|
aclRoleReq4 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole1Copy},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp4 structs.ACLRolesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq4, &aclRoleResp4)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp4.ACLRoles, 1)
|
|
require.True(t, aclRole1Copy.Equals(aclRoleResp4.ACLRoles[0]))
|
|
require.Greater(t, aclRoleResp4.ACLRoles[0].ModifyIndex, aclRoleResp3.ACLRoles[0].ModifyIndex)
|
|
|
|
// Create another ACL role that will fail validation. Attempting to upsert
|
|
// this ensures the handler is triggering the validation function.
|
|
aclRole2 := mock.ACLRole()
|
|
aclRole2.Policies = nil
|
|
|
|
aclRoleReq5 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole2},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRolesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
require.Error(t, err)
|
|
require.NotContains(t, err, "Permission denied")
|
|
|
|
// Try and create a role with a name that already exists within state.
|
|
aclRole3 := mock.ACLRole()
|
|
aclRole3.ID = ""
|
|
aclRole3.Name = aclRole1.Name
|
|
|
|
aclRoleReq6 := &structs.ACLRolesUpsertRequest{
|
|
ACLRoles: []*structs.ACLRole{aclRole3},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp6 structs.ACLRolesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq6, &aclRoleResp6)
|
|
require.ErrorContains(t, err, fmt.Sprintf("role with name %s already exists", aclRole1.Name))
|
|
}
|
|
|
|
func TestACL_DeleteRolesByID(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create two ACL roles and put these directly into state.
|
|
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
|
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
|
|
|
// Attempt to delete an ACL role without setting an auth token. This should
|
|
// fail.
|
|
aclRoleReq1 := &structs.ACLRolesDeleteByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRolesDeleteByIDResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
|
|
// Attempt to delete an ACL role now using a valid management token which
|
|
// should succeed.
|
|
aclRoleReq2 := &structs.ACLRolesDeleteByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRolesDeleteByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.NoError(t, err)
|
|
|
|
// Ensure the deleted role is not found within state and that the other is.
|
|
ws := memdb.NewWatchSet()
|
|
iter, err := testServer.State().GetACLRoles(ws)
|
|
require.NoError(t, err)
|
|
|
|
var aclRolesLookup []*structs.ACLRole
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
aclRolesLookup = append(aclRolesLookup, raw.(*structs.ACLRole))
|
|
}
|
|
|
|
require.Len(t, aclRolesLookup, 1)
|
|
require.True(t, aclRolesLookup[0].Equals(aclRoles[1]))
|
|
|
|
// Try to delete the previously deleted ACL role, this should fail.
|
|
aclRoleReq3 := &structs.ACLRolesDeleteByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRolesDeleteByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.ErrorContains(t, err, "ACL role not found")
|
|
}
|
|
|
|
func TestACL_ListRoles(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create two ACL roles with a known prefix and put these directly into
|
|
// state.
|
|
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
|
aclRoles[0].ID = "prefix-" + uuid.Generate()
|
|
aclRoles[1].ID = "prefix-" + uuid.Generate()
|
|
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
|
|
|
// Try listing roles without a valid ACL token.
|
|
aclRoleReq1 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: uuid.Generate(),
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRolesListResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "ACL token not found")
|
|
|
|
// Try listing roles with a valid ACL token.
|
|
aclRoleReq2 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRolesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp2.ACLRoles, 2)
|
|
|
|
// Try listing roles with a valid ACL token using a prefix that doesn't
|
|
// match anything.
|
|
aclRoleReq3 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
Prefix: "please",
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRolesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp3.ACLRoles, 0)
|
|
|
|
// Try listing roles with a valid ACL token using a prefix that matches two
|
|
// entries.
|
|
aclRoleReq4 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
Prefix: "prefix-",
|
|
},
|
|
}
|
|
var aclRoleResp4 structs.ACLRolesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq4, &aclRoleResp4)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp4.ACLRoles, 2)
|
|
|
|
// Generate and upsert an ACL Token which links to only one of the two
|
|
// roles within state.
|
|
aclToken := mock.ACLToken()
|
|
aclToken.Policies = nil
|
|
aclToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRoles[1].ID}}
|
|
|
|
err = testServer.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 20, []*structs.ACLToken{aclToken})
|
|
require.NoError(t, err)
|
|
|
|
aclRoleReq5 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRolesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp5.ACLRoles, 1)
|
|
require.Equal(t, aclRoleResp5.ACLRoles[0].ID, aclRoles[1].ID)
|
|
require.Equal(t, aclRoleResp5.ACLRoles[0].Name, aclRoles[1].Name)
|
|
|
|
// Now test a blocking query, where we wait for an update to the list which
|
|
// is triggered by a deletion.
|
|
type res struct {
|
|
err error
|
|
reply *structs.ACLRolesListResponse
|
|
}
|
|
resultCh := make(chan *res)
|
|
|
|
go func(resultCh chan *res) {
|
|
aclRoleReq6 := &structs.ACLRolesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
MinQueryIndex: aclRoleResp4.Index,
|
|
MaxQueryTime: 10 * time.Second,
|
|
},
|
|
}
|
|
var aclRoleResp6 structs.ACLRolesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq6, &aclRoleResp6)
|
|
resultCh <- &res{err: err, reply: &aclRoleResp6}
|
|
}(resultCh)
|
|
|
|
// Delete an ACL role from state which should return the blocking query.
|
|
require.NoError(t, testServer.fsm.State().DeleteACLRolesByID(
|
|
structs.MsgTypeTestSetup, aclRoleResp4.Index+10, []string{aclRoles[0].ID}))
|
|
|
|
// Wait until the test within the routine is complete.
|
|
result := <-resultCh
|
|
require.NoError(t, result.err)
|
|
require.Len(t, result.reply.ACLRoles, 1)
|
|
require.NotEqual(t, result.reply.ACLRoles[0].ID, aclRoles[0].ID)
|
|
}
|
|
|
|
func TestACL_GetRolesByID(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Try reading a role without setting a correct auth token.
|
|
aclRoleReq1 := &structs.ACLRolesByIDRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRolesByIDResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
require.Empty(t, aclRoleResp1.ACLRoles)
|
|
|
|
// Try reading a role that doesn't exist.
|
|
aclRoleReq2 := &structs.ACLRolesByIDRequest{
|
|
ACLRoleIDs: []string{"nope"},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.NoError(t, err)
|
|
require.Empty(t, aclRoleResp2.ACLRoles)
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create two ACL roles and put these directly into state.
|
|
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
|
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 20, aclRoles, false))
|
|
|
|
// Try reading both roles that are within state.
|
|
aclRoleReq3 := &structs.ACLRolesByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID, aclRoles[1].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp3.ACLRoles, 2)
|
|
require.Contains(t, aclRoleResp3.ACLRoles, aclRoles[0].ID)
|
|
require.Contains(t, aclRoleResp3.ACLRoles, aclRoles[1].ID)
|
|
|
|
// Now test a blocking query, where we wait for an update to the set which
|
|
// is triggered by a deletion.
|
|
type res struct {
|
|
err error
|
|
reply *structs.ACLRolesByIDResponse
|
|
}
|
|
resultCh := make(chan *res)
|
|
|
|
go func(resultCh chan *res) {
|
|
aclRoleReq5 := &structs.ACLRolesByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID, aclRoles[1].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
MinQueryIndex: aclRoleResp3.Index,
|
|
MaxQueryTime: 10 * time.Second,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
resultCh <- &res{err: err, reply: &aclRoleResp5}
|
|
}(resultCh)
|
|
|
|
// Delete an ACL role from state which should return the blocking query.
|
|
require.NoError(t, testServer.fsm.State().DeleteACLRolesByID(
|
|
structs.MsgTypeTestSetup, aclRoleResp3.Index+10, []string{aclRoles[0].ID}))
|
|
|
|
// Wait for the result and then test it.
|
|
result := <-resultCh
|
|
require.NoError(t, result.err)
|
|
require.Len(t, result.reply.ACLRoles, 1)
|
|
_, ok := result.reply.ACLRoles[aclRoles[1].ID]
|
|
require.True(t, ok)
|
|
}
|
|
|
|
func TestACL_GetRoleByID(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create two ACL roles and put these directly into state.
|
|
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
|
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
|
|
|
// Try reading a role without setting a correct auth token.
|
|
aclRoleReq1 := &structs.ACLRoleByIDRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRoleByIDResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
|
|
// Try reading a role that doesn't exist.
|
|
aclRoleReq2 := &structs.ACLRoleByIDRequest{
|
|
RoleID: "nope",
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRoleByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.NoError(t, err)
|
|
require.Nil(t, aclRoleResp2.ACLRole)
|
|
|
|
// Read both our available ACL roles using a valid auth token.
|
|
aclRoleReq3 := &structs.ACLRoleByIDRequest{
|
|
RoleID: aclRoles[0].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRoleByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.NoError(t, err)
|
|
require.True(t, aclRoleResp3.ACLRole.Equals(aclRoles[0]))
|
|
|
|
aclRoleReq4 := &structs.ACLRoleByIDRequest{
|
|
RoleID: aclRoles[1].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp4 structs.ACLRoleByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq4, &aclRoleResp4)
|
|
require.NoError(t, err)
|
|
require.True(t, aclRoleResp4.ACLRole.Equals(aclRoles[1]))
|
|
|
|
// Generate and upsert an ACL Token which links to only one of the two
|
|
// roles within state.
|
|
aclToken := mock.ACLToken()
|
|
aclToken.Policies = nil
|
|
aclToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRoles[1].ID}}
|
|
|
|
err = testServer.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 20, []*structs.ACLToken{aclToken})
|
|
require.NoError(t, err)
|
|
|
|
// Try detailing the role that is tried to our ACL token.
|
|
aclRoleReq5 := &structs.ACLRoleByIDRequest{
|
|
RoleID: aclRoles[1].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRoleByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, aclRoleResp5.ACLRole)
|
|
require.Equal(t, aclRoleResp5.ACLRole.ID, aclRoles[1].ID)
|
|
|
|
// Try detailing the role that is NOT tried to our ACL token.
|
|
aclRoleReq6 := &structs.ACLRoleByIDRequest{
|
|
RoleID: aclRoles[0].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp6 structs.ACLRoleByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq6, &aclRoleResp6)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
}
|
|
|
|
func TestACL_GetRoleByName(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.Name = "mocked-test-policy-1"
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.Name = "mocked-test-policy-2"
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
|
|
|
// Create two ACL roles and put these directly into state.
|
|
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
|
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles, false))
|
|
|
|
// Try reading a role without setting a correct auth token.
|
|
aclRoleReq1 := &structs.ACLRoleByNameRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclRoleResp1 structs.ACLRoleByNameResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq1, &aclRoleResp1)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
|
|
// Try reading a role that doesn't exist.
|
|
aclRoleReq2 := &structs.ACLRoleByNameRequest{
|
|
RoleName: "nope",
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp2 structs.ACLRoleByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq2, &aclRoleResp2)
|
|
require.NoError(t, err)
|
|
require.Nil(t, aclRoleResp2.ACLRole)
|
|
|
|
// Read both our available ACL roles using a valid auth token.
|
|
aclRoleReq3 := &structs.ACLRoleByNameRequest{
|
|
RoleName: aclRoles[0].Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp3 structs.ACLRoleByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq3, &aclRoleResp3)
|
|
require.NoError(t, err)
|
|
require.True(t, aclRoleResp3.ACLRole.Equals(aclRoles[0]))
|
|
|
|
aclRoleReq4 := &structs.ACLRoleByNameRequest{
|
|
RoleName: aclRoles[1].Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp4 structs.ACLRoleByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq4, &aclRoleResp4)
|
|
require.NoError(t, err)
|
|
require.True(t, aclRoleResp4.ACLRole.Equals(aclRoles[1]))
|
|
|
|
// Generate and upsert an ACL Token which links to only one of the two
|
|
// roles within state.
|
|
aclToken := mock.ACLToken()
|
|
aclToken.Policies = nil
|
|
aclToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRoles[1].ID}}
|
|
|
|
err = testServer.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 20, []*structs.ACLToken{aclToken})
|
|
require.NoError(t, err)
|
|
|
|
// Try detailing the role that is tried to our ACL token.
|
|
aclRoleReq5 := &structs.ACLRoleByNameRequest{
|
|
RoleName: aclRoles[1].Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRoleByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, aclRoleResp5.ACLRole)
|
|
require.Equal(t, aclRoleResp5.ACLRole.ID, aclRoles[1].ID)
|
|
require.Equal(t, aclRoleResp5.ACLRole.Name, aclRoles[1].Name)
|
|
|
|
// Try detailing the role that is NOT tried to our ACL token.
|
|
aclRoleReq6 := &structs.ACLRoleByNameRequest{
|
|
RoleName: aclRoles[0].Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclToken.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp6 structs.ACLRoleByNameResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq6, &aclRoleResp6)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
}
|