3052ddf8f1
When a user performs a client API call, the Nomad client will perform an RPC which looks up the ACL policies which the callers ACL token is assigned. If the ACL token includes dangling (deleted) policies, the call would previously fail with a permission denied error. This change ensures this error is not returned and that the lookup will succeed in the event of dangling policies. Co-authored-by: James Rasell <jrasell@users.noreply.github.com>
3909 lines
130 KiB
Go
3909 lines
130 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
capOIDC "github.com/hashicorp/cap/oidc"
|
|
"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())
|
|
|
|
// Generate and upsert an ACL role which links to the previously created
|
|
// policy.
|
|
mockACLRole := mock.ACLRole()
|
|
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: policy.Name}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 1010, []*structs.ACLRole{mockACLRole}, false))
|
|
|
|
// Generate and upsert an ACL token which only has ACL role links.
|
|
mockTokenWithRole := mock.ACLToken()
|
|
mockTokenWithRole.Policies = []string{}
|
|
mockTokenWithRole.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole.ID}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 1020, []*structs.ACLToken{mockTokenWithRole}))
|
|
|
|
// Use the newly created token to attempt to read the policy which is
|
|
// linked via a role, and not directly referenced within the policy array.
|
|
req5 := &structs.ACLPolicySpecificRequest{
|
|
Name: policy.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: mockTokenWithRole.SecretID,
|
|
},
|
|
}
|
|
|
|
var resp5 structs.SingleACLPolicyResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicy", req5, &resp5))
|
|
must.Eq(t, 1000, resp5.Index)
|
|
must.Eq(t, policy, resp5.Policy)
|
|
}
|
|
|
|
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()
|
|
must.NoError(t, s1.fsm.State().UpsertACLPolicies(structs.MsgTypeTestSetup, 1000, []*structs.ACLPolicy{policy, policy2}))
|
|
|
|
token := mock.ACLToken()
|
|
token.Policies = []string{policy.Name}
|
|
must.NoError(t, 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
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp))
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
must.Eq(t, 1, len(resp.Policies))
|
|
must.Eq(t, policy, resp.Policies[policy.Name])
|
|
|
|
// Lookup non-associated policy
|
|
get.Names = []string{policy2.Name}
|
|
resp = structs.ACLPolicySetResponse{}
|
|
must.Error(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", get, &resp))
|
|
|
|
// Generate and upsert an ACL role which links to the previously created
|
|
// policy.
|
|
mockACLRole := mock.ACLRole()
|
|
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: policy.Name}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 1010, []*structs.ACLRole{mockACLRole}, false))
|
|
|
|
// Generate and upsert an ACL token which only has ACL role links.
|
|
mockTokenWithRole := mock.ACLToken()
|
|
mockTokenWithRole.Policies = []string{}
|
|
mockTokenWithRole.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole.ID}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 1020, []*structs.ACLToken{mockTokenWithRole}))
|
|
|
|
// Use the newly created token to attempt to read the policy which is
|
|
// linked via a role, and not directly referenced within the policy array.
|
|
req1 := &structs.ACLPolicySetRequest{
|
|
Names: []string{policy.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: mockTokenWithRole.SecretID,
|
|
},
|
|
}
|
|
|
|
var resp1 structs.ACLPolicySetResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", req1, &resp1))
|
|
must.Eq(t, 1000, resp1.Index)
|
|
must.Eq(t, 1, len(resp1.Policies))
|
|
must.Eq(t, policy, resp1.Policies[policy.Name])
|
|
|
|
// Generate and upsert an ACL token which only has both direct policy links
|
|
// and ACL role links.
|
|
mockTokenWithRolePolicy := mock.ACLToken()
|
|
mockTokenWithRolePolicy.Policies = []string{policy2.Name}
|
|
mockTokenWithRolePolicy.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole.ID}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 1030, []*structs.ACLToken{mockTokenWithRolePolicy}))
|
|
|
|
// Use the newly created token to attempt to read the policies which are
|
|
// linked directly, and by ACL roles.
|
|
req2 := &structs.ACLPolicySetRequest{
|
|
Names: []string{policy.Name, policy2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: mockTokenWithRolePolicy.SecretID,
|
|
},
|
|
}
|
|
|
|
var resp2 structs.ACLPolicySetResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", req2, &resp2))
|
|
must.Eq(t, 1000, resp2.Index)
|
|
must.Eq(t, 2, len(resp2.Policies))
|
|
|
|
// Delete one of the policies, which means the ACL token has a dangling
|
|
// policy. When a Nomad client perform an ACL lookup, it adds the policies
|
|
// attached to the token within the request arguments. This test section
|
|
// mimics the behaviour when a token is being used that contains dangling
|
|
// policies.
|
|
must.NoError(t, s1.fsm.State().DeleteACLPolicies(structs.MsgTypeTestSetup, 1040, []string{policy.Name}))
|
|
|
|
req3 := &structs.ACLPolicySetRequest{
|
|
Names: []string{policy.Name, policy2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: mockTokenWithRolePolicy.SecretID,
|
|
},
|
|
}
|
|
|
|
var resp3 structs.ACLPolicySetResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.GetPolicies", req3, &resp3))
|
|
must.Eq(t, 1040, resp3.Index)
|
|
must.MapLen(t, 1, resp3.Policies)
|
|
must.MapContainsKey(t, resp3.Policies, policy2.Name)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Generate and upsert an ACL role which links to the previously created
|
|
// policy.
|
|
mockACLRole := mock.ACLRole()
|
|
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: p1.Name}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 1010, []*structs.ACLRole{mockACLRole}, false))
|
|
|
|
// Generate and upsert an ACL token which only has ACL role links.
|
|
mockTokenWithRole := mock.ACLToken()
|
|
mockTokenWithRole.Policies = []string{}
|
|
mockTokenWithRole.Roles = []*structs.ACLTokenRoleLink{{ID: mockACLRole.ID}}
|
|
must.NoError(t, s1.fsm.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 1020, []*structs.ACLToken{mockTokenWithRole}))
|
|
|
|
// Use the newly created token to attempt to list the policies. We should
|
|
// get the single policy linked by the ACL role.
|
|
req4 := &structs.ACLPolicyListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: mockTokenWithRole.SecretID,
|
|
},
|
|
}
|
|
|
|
var resp4 structs.ACLPolicyListResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, "ACL.ListPolicies", req4, &resp4))
|
|
must.Eq(t, 1000, resp4.Index)
|
|
must.Len(t, 1, resp4.Policies)
|
|
must.Eq(t, p1.Name, resp4.Policies[0].Name)
|
|
must.Eq(t, p1.Hash, resp4.Policies[0].Hash)
|
|
}
|
|
|
|
// 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)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "failed to parse")
|
|
}
|
|
|
|
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, os.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.
|
|
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.Positive(t, resp.Index)
|
|
|
|
// 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.Equal(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.Equal(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].Equal(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, structs.ErrPermissionDenied.Error())
|
|
|
|
// 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{
|
|
ACLRoleIDs: []string{"nope"},
|
|
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)
|
|
|
|
// Create a client token which allows us to test client tokens looking up
|
|
// their own role assignments.
|
|
clientToken1 := &structs.ACLToken{
|
|
AccessorID: uuid.Generate(),
|
|
SecretID: uuid.Generate(),
|
|
Name: "acl-endpoint-test-role",
|
|
Type: structs.ACLClientToken,
|
|
Roles: []*structs.ACLTokenRoleLink{{ID: aclRoles[0].ID}},
|
|
}
|
|
clientToken1.SetHash()
|
|
|
|
require.NoError(t, testServer.fsm.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{clientToken1}))
|
|
|
|
// Use the client token in an attempt to look up an ACL role which is
|
|
// assigned to the token, and therefore should work.
|
|
aclRoleReq4 := &structs.ACLRolesByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[0].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: clientToken1.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp4 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq4, &aclRoleResp4)
|
|
require.NoError(t, err)
|
|
require.Len(t, aclRoleResp4.ACLRoles, 1)
|
|
require.Contains(t, aclRoleResp4.ACLRoles, aclRoles[0].ID)
|
|
|
|
// Use the client token in an attempt to look up an ACL role which is NOT
|
|
// assigned to the token which should fail.
|
|
aclRoleReq5 := &structs.ACLRolesByIDRequest{
|
|
ACLRoleIDs: []string{aclRoles[1].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: clientToken1.SecretID,
|
|
},
|
|
}
|
|
var aclRoleResp5 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, aclRoleReq5, &aclRoleResp5)
|
|
require.ErrorContains(t, err, "Permission denied")
|
|
|
|
// 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) {
|
|
aclRoleReq6 := &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 aclRoleResp6 structs.ACLRolesByIDResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRolesByIDRPCMethod, 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, 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.Equal(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.Equal(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.Equal(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.Equal(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")
|
|
}
|
|
|
|
func TestACLEndpoint_GetAuthMethod(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
|
|
authMethod := mock.ACLOIDCAuthMethod()
|
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod}))
|
|
|
|
anonymousAuthMethod := mock.ACLOIDCAuthMethod()
|
|
anonymousAuthMethod.Name = "anonymous"
|
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1001, []*structs.ACLAuthMethod{anonymousAuthMethod}))
|
|
|
|
// Lookup the authMethod
|
|
get := &structs.ACLAuthMethodGetRequest{
|
|
MethodName: authMethod.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodGetResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodRPCMethod, get, &resp))
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
must.Eq(t, authMethod, resp.AuthMethod)
|
|
|
|
// Lookup non-existing authMethod
|
|
get.MethodName = uuid.Generate()
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodRPCMethod, get, &resp))
|
|
must.Eq(t, uint64(1001), resp.Index)
|
|
must.Nil(t, resp.AuthMethod)
|
|
}
|
|
|
|
func TestACLEndpoint_GetAuthMethod_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 authMethods
|
|
am1 := mock.ACLOIDCAuthMethod()
|
|
am2 := mock.ACLOIDCAuthMethod()
|
|
|
|
// First create an unrelated authMethod
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.UpsertACLAuthMethods(100, []*structs.ACLAuthMethod{am1}))
|
|
})
|
|
|
|
// Upsert the authMethod we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
must.NoError(t, state.UpsertACLAuthMethods(200, []*structs.ACLAuthMethod{am2}))
|
|
})
|
|
|
|
// Lookup the authMethod
|
|
req := &structs.ACLAuthMethodGetRequest{
|
|
MethodName: am2.Name,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodGetResponse
|
|
start := time.Now()
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodRPCMethod, req, &resp))
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
must.Eq(t, resp.Index, 200)
|
|
must.NotNil(t, resp.AuthMethod)
|
|
must.Eq(t, resp.AuthMethod.Name, am2.Name)
|
|
|
|
// Auth method delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.DeleteACLAuthMethods(300, []string{am2.Name}))
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.ACLAuthMethodGetResponse
|
|
start = time.Now()
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodRPCMethod, req, &resp2))
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
must.Eq(t, resp2.Index, 300)
|
|
must.Nil(t, resp2.AuthMethod)
|
|
}
|
|
|
|
func TestACLEndpoint_GetAuthMethods(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
|
|
authMethod := mock.ACLOIDCAuthMethod()
|
|
authMethod2 := mock.ACLOIDCAuthMethod()
|
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{authMethod, authMethod2}))
|
|
|
|
// Lookup the authMethod
|
|
get := &structs.ACLAuthMethodsGetRequest{
|
|
Names: []string{authMethod.Name, authMethod2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodsGetResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodsRPCMethod, get, &resp))
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
must.Eq(t, 2, len(resp.AuthMethods))
|
|
must.Eq(t, authMethod, resp.AuthMethods[authMethod.Name])
|
|
must.Eq(t, authMethod2, resp.AuthMethods[authMethod2.Name])
|
|
|
|
// Lookup non-existing authMethod
|
|
get.Names = []string{uuid.Generate()}
|
|
resp = structs.ACLAuthMethodsGetResponse{}
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodsRPCMethod, get, &resp))
|
|
must.Eq(t, uint64(1000), resp.Index)
|
|
must.Eq(t, 0, len(resp.AuthMethods))
|
|
}
|
|
|
|
func TestACLEndpoint_GetAuthMethods_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 authMethods
|
|
am1 := mock.ACLOIDCAuthMethod()
|
|
am2 := mock.ACLOIDCAuthMethod()
|
|
|
|
// First create an unrelated authMethod
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.UpsertACLAuthMethods(100, []*structs.ACLAuthMethod{am1}))
|
|
})
|
|
|
|
// Upsert the authMethod we are watching later
|
|
time.AfterFunc(200*time.Millisecond, func() {
|
|
must.NoError(t, state.UpsertACLAuthMethods(200, []*structs.ACLAuthMethod{am2}))
|
|
})
|
|
|
|
// Lookup the authMethod
|
|
req := &structs.ACLAuthMethodsGetRequest{
|
|
Names: []string{am2.Name},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 150,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodsGetResponse
|
|
start := time.Now()
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodsRPCMethod, req, &resp))
|
|
|
|
if elapsed := time.Since(start); elapsed < 200*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
must.Eq(t, resp.Index, 200)
|
|
must.NotEq(t, len(resp.AuthMethods), 0)
|
|
must.NotNil(t, resp.AuthMethods[am2.Name])
|
|
|
|
// Auth method delete triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.DeleteACLAuthMethods(300, []string{am2.Name}))
|
|
})
|
|
|
|
req.QueryOptions.MinQueryIndex = 250
|
|
var resp2 structs.ACLAuthMethodsGetResponse
|
|
start = time.Now()
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLGetAuthMethodsRPCMethod, req, &resp2))
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
must.Eq(t, resp2.Index, 300)
|
|
must.Eq(t, len(resp2.AuthMethods), 0)
|
|
}
|
|
|
|
func TestACLEndpoint_ListAuthMethods(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
|
|
am1 := mock.ACLOIDCAuthMethod()
|
|
am2 := mock.ACLOIDCAuthMethod()
|
|
|
|
am1.Name = "aaaaaaaa-3350-4b4b-d185-0e1992ed43e9"
|
|
am2.Name = "aaaabbbb-3350-4b4b-d185-0e1992ed43e9"
|
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1, am2}))
|
|
|
|
// Create a token
|
|
token := mock.ACLToken()
|
|
must.NoError(t, s1.fsm.State().UpsertACLTokens(structs.MsgTypeTestSetup, 1001, []*structs.ACLToken{token}))
|
|
|
|
// Lookup the authMethods with a management token
|
|
get := &structs.ACLAuthMethodListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodListResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLListAuthMethodsRPCMethod, get, &resp))
|
|
must.Eq(t, 1000, resp.Index)
|
|
must.Len(t, 2, resp.AuthMethods)
|
|
|
|
// List authMethods using the created token
|
|
get = &structs.ACLAuthMethodListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
AuthToken: token.SecretID,
|
|
},
|
|
}
|
|
var resp3 structs.ACLAuthMethodListResponse
|
|
if err := msgpackrpc.CallWithCodec(codec, structs.ACLListAuthMethodsRPCMethod, get, &resp3); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
must.Eq(t, 1000, resp3.Index)
|
|
must.Len(t, 2, resp3.AuthMethods)
|
|
must.Eq(t, resp3.AuthMethods[0].Name, am1.Name)
|
|
}
|
|
|
|
func TestACLEndpoint_ListAuthMethods_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 authMethod
|
|
authMethod := mock.ACLOIDCAuthMethod()
|
|
|
|
// Upsert auth method triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.UpsertACLAuthMethods(2, []*structs.ACLAuthMethod{authMethod}))
|
|
})
|
|
|
|
req := &structs.ACLAuthMethodListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "global",
|
|
MinQueryIndex: 1,
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
start := time.Now()
|
|
var resp structs.ACLAuthMethodListResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLListAuthMethodsRPCMethod, req, &resp))
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp)
|
|
}
|
|
must.Eq(t, uint64(2), resp.Index)
|
|
must.Len(t, 1, resp.AuthMethods)
|
|
must.Eq(t, resp.AuthMethods[0].Name, authMethod.Name)
|
|
|
|
// Eval deletion triggers watches
|
|
time.AfterFunc(100*time.Millisecond, func() {
|
|
must.NoError(t, state.DeleteACLAuthMethods(3, []string{authMethod.Name}))
|
|
})
|
|
|
|
req.MinQueryIndex = 2
|
|
start = time.Now()
|
|
var resp2 structs.ACLAuthMethodListResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLListAuthMethodsRPCMethod, req, &resp2))
|
|
|
|
if elapsed := time.Since(start); elapsed < 100*time.Millisecond {
|
|
t.Fatalf("should block (returned in %s) %#v", elapsed, resp2)
|
|
}
|
|
must.Eq(t, uint64(3), resp2.Index)
|
|
must.Eq(t, 0, len(resp2.AuthMethods))
|
|
}
|
|
|
|
func TestACLEndpoint_DeleteAuthMethods(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
|
|
am1 := mock.ACLOIDCAuthMethod()
|
|
must.NoError(t, s1.fsm.State().UpsertACLAuthMethods(1000, []*structs.ACLAuthMethod{am1}))
|
|
|
|
// Lookup the authMethods
|
|
req := &structs.ACLAuthMethodDeleteRequest{
|
|
Names: []string{am1.Name},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodDeleteResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLDeleteAuthMethodsRPCMethod, req, &resp))
|
|
must.NotEq(t, uint64(0), resp.Index)
|
|
|
|
// Try to delete a non-existing auth method
|
|
req = &structs.ACLAuthMethodDeleteRequest{
|
|
Names: []string{"non-existing-auth-method"},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp2 structs.ACLAuthMethodDeleteResponse
|
|
must.Error(t, msgpackrpc.CallWithCodec(codec, structs.ACLDeleteAuthMethodsRPCMethod, req, &resp2))
|
|
}
|
|
|
|
func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
s1, root, cleanupS1 := TestACLServer(t, nil)
|
|
defer cleanupS1()
|
|
codec := rpcClient(t, s1)
|
|
testutil.WaitForLeader(t, s1.RPC)
|
|
|
|
minTTL, _ := time.ParseDuration("10s")
|
|
maxTTL, _ := time.ParseDuration("24h")
|
|
s1.config.ACLTokenMinExpirationTTL = minTTL
|
|
s1.config.ACLTokenMaxExpirationTTL = maxTTL
|
|
|
|
// Create the register request
|
|
am1 := mock.ACLOIDCAuthMethod()
|
|
am1.Default = true // make sure it's going to be a default method
|
|
am1.SetHash()
|
|
|
|
// Lookup the authMethods
|
|
req := &structs.ACLAuthMethodUpsertRequest{
|
|
AuthMethods: []*structs.ACLAuthMethod{am1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
var resp structs.ACLAuthMethodUpsertResponse
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertAuthMethodsRPCMethod, req, &resp))
|
|
must.NotEq(t, uint64(0), resp.Index)
|
|
|
|
// Check we created the authMethod
|
|
out, err := s1.fsm.State().GetACLAuthMethodByName(nil, am1.Name)
|
|
must.Nil(t, err)
|
|
must.NotNil(t, out)
|
|
must.NotEq(t, 0, len(resp.AuthMethods))
|
|
must.True(t, am1.Equal(resp.AuthMethods[0]))
|
|
|
|
// Try to insert another default authMethod
|
|
am2 := mock.ACLOIDCAuthMethod()
|
|
am2.Default = true
|
|
req = &structs.ACLAuthMethodUpsertRequest{
|
|
AuthMethods: []*structs.ACLAuthMethod{am2},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
// We expect this to err since there's already a default method of the same type
|
|
must.Error(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertAuthMethodsRPCMethod, req, &resp))
|
|
|
|
// Update token locality
|
|
am3 := &structs.ACLAuthMethod{Name: am1.Name, TokenLocality: "global"}
|
|
req = &structs.ACLAuthMethodUpsertRequest{
|
|
AuthMethods: []*structs.ACLAuthMethod{am3},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: root.SecretID,
|
|
},
|
|
}
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertAuthMethodsRPCMethod, req, &resp))
|
|
must.Eq(t, resp.AuthMethods[0].TokenLocality, am3.TokenLocality)
|
|
}
|
|
|
|
func TestACL_UpsertBindingRules(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 binding rule and remove the ID so this looks like a
|
|
// creation.
|
|
aclBindingRule1 := mock.ACLBindingRule()
|
|
aclBindingRule1.ID = ""
|
|
|
|
// Attempt to upsert this binding rule without setting an ACL token. This
|
|
// should fail.
|
|
aclBindingRuleReq1 := &structs.ACLBindingRulesUpsertRequest{
|
|
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
},
|
|
}
|
|
var aclBindingRuleResp1 structs.ACLBindingRulesUpsertResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
|
|
must.EqError(t, err, "Permission denied")
|
|
|
|
// Attempt to upsert this binding rule that references a auth method that
|
|
// does not exist in state.
|
|
aclBindingRuleReq2 := &structs.ACLBindingRulesUpsertRequest{
|
|
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp2 structs.ACLBindingRulesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
|
|
must.EqError(t, err, "RPC Error:: 400,ACL auth method auth0 not found")
|
|
|
|
// Create the policies our ACL roles wants to link to.
|
|
authMethod := mock.ACLOIDCAuthMethod()
|
|
authMethod.Name = aclBindingRule1.AuthMethod
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
|
|
|
|
// Try the upsert a third time, which should succeed.
|
|
aclBindingRuleReq3 := &structs.ACLBindingRulesUpsertRequest{
|
|
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp3 structs.ACLBindingRulesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
|
|
must.NoError(t, err)
|
|
must.Len(t, 1, aclBindingRuleResp3.ACLBindingRules)
|
|
|
|
// Perform an update of the ACL binding rule by updating the description.
|
|
aclBindingRule1Copy := aclBindingRule1.Copy()
|
|
aclBindingRule1Copy.Description = "updated-description"
|
|
|
|
aclBindingRuleReq4 := &structs.ACLBindingRulesUpsertRequest{
|
|
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: "global",
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp4 structs.ACLBindingRulesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
|
|
must.NoError(t, err)
|
|
must.Len(t, 1, aclBindingRuleResp4.ACLBindingRules)
|
|
must.Greater(t, aclBindingRuleResp3.ACLBindingRules[0].ModifyIndex, aclBindingRuleResp4.ACLBindingRules[0].ModifyIndex)
|
|
|
|
// Create another ACL binding rule that will fail validation. Attempting to
|
|
// upsert this ensures the handler is triggering the validation function.
|
|
aclBindingRule2 := mock.ACLBindingRule()
|
|
aclBindingRule2.ID = ""
|
|
aclBindingRule2.BindType = ""
|
|
|
|
aclBindingRuleReq5 := &structs.ACLBindingRulesUpsertRequest{
|
|
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule2},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp5 structs.ACLBindingRulesUpsertResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq5, &aclBindingRuleResp5)
|
|
must.Error(t, err)
|
|
must.StrContains(t, err.Error(), "bind type is missing")
|
|
}
|
|
|
|
func TestACL_DeleteBindingRules(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create two ACL binding rules and put these directly into state.
|
|
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
|
|
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
|
|
|
|
// Attempt to delete an ACL binding rule without setting an auth token.
|
|
// This should fail.
|
|
aclBindingRuleReq1 := &structs.ACLBindingRulesDeleteRequest{
|
|
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclBindingRuleResp1 structs.ACLBindingRulesDeleteResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
|
|
must.EqError(t, err, "Permission denied")
|
|
|
|
// Attempt to delete an ACL binding rule now using a valid management token
|
|
// which should succeed.
|
|
aclBindingRuleReq2 := &structs.ACLBindingRulesDeleteRequest{
|
|
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp2 structs.ACLBindingRulesDeleteResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
|
|
must.NoError(t, err)
|
|
|
|
// Ensure the deleted binding rule is not found within state and that the
|
|
// other is.
|
|
ws := memdb.NewWatchSet()
|
|
iter, err := testServer.State().GetACLBindingRules(ws)
|
|
must.NoError(t, err)
|
|
|
|
var aclBindingRulesLookup []*structs.ACLBindingRule
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
aclBindingRulesLookup = append(aclBindingRulesLookup, raw.(*structs.ACLBindingRule))
|
|
}
|
|
|
|
must.Len(t, 1, aclBindingRulesLookup)
|
|
must.Eq(t, aclBindingRulesLookup[0], aclBindingRules[1])
|
|
|
|
// Try to delete the previously deleted ACL binding rule, this should fail.
|
|
aclBindingRuleReq3 := &structs.ACLBindingRulesDeleteRequest{
|
|
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp3 structs.ACLBindingRulesDeleteResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
|
|
must.EqError(t, err, "ACL binding rule not found")
|
|
}
|
|
|
|
func TestACL_ListBindingRules(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create two ACL binding rules and put these directly into state.
|
|
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
|
|
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
|
|
|
|
// Try listing binding rules without a valid ACL token.
|
|
aclBindingRuleReq1 := &structs.ACLBindingRulesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclBindingRuleResp1 structs.ACLBindingRulesListResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
|
|
must.EqError(t, err, "Permission denied")
|
|
|
|
// Try listing roles with a valid ACL token.
|
|
aclBindingRuleReq2 := &structs.ACLBindingRulesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp2 structs.ACLBindingRulesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
|
|
must.NoError(t, err)
|
|
must.Len(t, 2, aclBindingRuleResp2.ACLBindingRules)
|
|
|
|
// 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.ACLBindingRulesListResponse
|
|
}
|
|
resultCh := make(chan *res)
|
|
|
|
go func(resultCh chan *res) {
|
|
aclBindingRuleReq3 := &structs.ACLBindingRulesListRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
MinQueryIndex: aclBindingRuleResp2.Index,
|
|
MaxQueryTime: 10 * time.Second,
|
|
},
|
|
}
|
|
var aclBindingRuleResp3 structs.ACLBindingRulesListResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
|
|
resultCh <- &res{err: err, reply: &aclBindingRuleResp3}
|
|
}(resultCh)
|
|
|
|
// Delete an ACL binding rule from state which should return the blocking
|
|
// query.
|
|
must.NoError(t, testServer.fsm.State().DeleteACLBindingRules(
|
|
aclBindingRuleResp2.Index+10, []string{aclBindingRules[0].ID}))
|
|
|
|
// Wait until the test within the routine is complete.
|
|
result := <-resultCh
|
|
must.NoError(t, result.err)
|
|
must.Len(t, 1, result.reply.ACLBindingRules)
|
|
must.NotEq(t, result.reply.ACLBindingRules[0].ID, aclBindingRules[0].ID)
|
|
must.Greater(t, aclBindingRuleResp2.Index, result.reply.Index)
|
|
}
|
|
|
|
func TestACL_GetBindingRules(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 binding rule without setting a correct auth token.
|
|
aclBindingRuleReq1 := &structs.ACLBindingRulesRequest{
|
|
ACLBindingRuleIDs: []string{"nope"},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclBindingRuleResp1 structs.ACLBindingRulesResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
|
|
must.EqError(t, err, "Permission denied")
|
|
must.MapEmpty(t, aclBindingRuleResp1.ACLBindingRules)
|
|
|
|
// Try reading a binding rule that doesn't exist.
|
|
aclBindingRuleReq2 := &structs.ACLBindingRulesRequest{
|
|
ACLBindingRuleIDs: []string{"nope"},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp2 structs.ACLBindingRulesResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
|
|
must.NoError(t, err)
|
|
must.MapEmpty(t, aclBindingRuleResp1.ACLBindingRules)
|
|
|
|
// Create two ACL binding rules and put these directly into state.
|
|
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
|
|
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
|
|
|
|
// Try reading both binding rules that are within state.
|
|
aclBindingRuleReq3 := &structs.ACLBindingRulesRequest{
|
|
ACLBindingRuleIDs: []string{aclBindingRules[0].ID, aclBindingRules[1].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp3 structs.ACLBindingRulesResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
|
|
must.NoError(t, err)
|
|
must.MapLen(t, 2, aclBindingRuleResp3.ACLBindingRules)
|
|
must.MapContainsKeys(t, aclBindingRuleResp3.ACLBindingRules, []string{aclBindingRules[0].ID, aclBindingRules[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.ACLBindingRulesResponse
|
|
}
|
|
resultCh := make(chan *res)
|
|
|
|
go func(resultCh chan *res) {
|
|
aclBindingRuleReq4 := &structs.ACLBindingRulesRequest{
|
|
ACLBindingRuleIDs: []string{aclBindingRules[0].ID, aclBindingRules[1].ID},
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
MinQueryIndex: aclBindingRuleResp3.Index,
|
|
MaxQueryTime: 10 * time.Second,
|
|
},
|
|
}
|
|
var aclBindingRuleResp4 structs.ACLBindingRulesResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
|
|
resultCh <- &res{err: err, reply: &aclBindingRuleResp4}
|
|
}(resultCh)
|
|
|
|
// Delete an ACL role from state which should return the blocking query.
|
|
must.NoError(t, testServer.fsm.State().DeleteACLBindingRules(
|
|
aclBindingRuleResp3.Index+10, []string{aclBindingRules[0].ID}))
|
|
|
|
// Wait for the result and then test it.
|
|
result := <-resultCh
|
|
must.NoError(t, result.err)
|
|
must.MapLen(t, 1, result.reply.ACLBindingRules)
|
|
must.MapContainsKeys(t, result.reply.ACLBindingRules, []string{aclBindingRules[1].ID})
|
|
must.Greater(t, aclBindingRuleResp3.Index, result.reply.Index)
|
|
}
|
|
|
|
func TestACL_GetBindingRule(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create two ACL binding rules and put these directly into state.
|
|
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
|
|
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
|
|
|
|
// Try reading a role without setting a correct auth token.
|
|
aclBindingRuleReq1 := &structs.ACLBindingRuleRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
var aclBindingRuleResp1 structs.ACLBindingRuleResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
|
|
must.EqError(t, err, "Permission denied")
|
|
|
|
// Try reading a role that doesn't exist.
|
|
aclBindingRuleReq2 := &structs.ACLBindingRuleRequest{
|
|
ACLBindingRuleID: "nope",
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp2 structs.ACLBindingRuleResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
|
|
must.NoError(t, err)
|
|
must.Nil(t, aclBindingRuleResp2.ACLBindingRule)
|
|
|
|
// Read both our available ACL roles using a valid auth token.
|
|
aclBindingRuleReq3 := &structs.ACLBindingRuleRequest{
|
|
ACLBindingRuleID: aclBindingRules[0].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp3 structs.ACLBindingRuleResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
|
|
must.NoError(t, err)
|
|
must.Eq(t, aclBindingRules[0].ID, aclBindingRuleResp3.ACLBindingRule.ID)
|
|
|
|
aclBindingRuleReq4 := &structs.ACLBindingRuleRequest{
|
|
ACLBindingRuleID: aclBindingRules[1].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
},
|
|
}
|
|
var aclBindingRuleResp4 structs.ACLBindingRuleResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
|
|
must.NoError(t, err)
|
|
must.Eq(t, aclBindingRules[1].ID, aclBindingRuleResp4.ACLBindingRule.ID)
|
|
|
|
// Now test a blocking query, where we wait for an update to the set which
|
|
// is triggered by an upsert.
|
|
type res struct {
|
|
err error
|
|
reply *structs.ACLBindingRuleResponse
|
|
}
|
|
resultCh := make(chan *res)
|
|
|
|
go func(resultCh chan *res) {
|
|
aclBindingRuleReq5 := &structs.ACLBindingRuleRequest{
|
|
ACLBindingRuleID: aclBindingRules[0].ID,
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: DefaultRegion,
|
|
AuthToken: aclRootToken.SecretID,
|
|
MinQueryIndex: aclBindingRuleResp4.Index,
|
|
MaxQueryTime: 10 * time.Second,
|
|
},
|
|
}
|
|
var aclBindingRuleResp5 structs.ACLBindingRuleResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq5, &aclBindingRuleResp5)
|
|
resultCh <- &res{err: err, reply: &aclBindingRuleResp5}
|
|
}(resultCh)
|
|
|
|
// Delete an ACL role from state which should return the blocking query.
|
|
aclBindingRule1Copy := aclBindingRules[0].Copy()
|
|
aclBindingRule1Copy.Description = "updated-description"
|
|
aclBindingRule1Copy.SetHash()
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
|
aclBindingRuleResp4.Index+10, []*structs.ACLBindingRule{aclBindingRule1Copy}, true))
|
|
|
|
// Wait for the result and then test it.
|
|
result := <-resultCh
|
|
must.NoError(t, result.err)
|
|
must.Eq(t, aclBindingRules[0].ID, result.reply.ACLBindingRule.ID)
|
|
must.Greater(t, aclBindingRuleResp4.Index, result.reply.Index)
|
|
}
|
|
|
|
func TestACL_OIDCAuthURL(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, _, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Set up the test OIDC provider.
|
|
oidcTestProvider := capOIDC.StartTestProvider(t)
|
|
defer oidcTestProvider.Stop()
|
|
oidcTestProvider.SetClientCreds("bob", "topsecretcredthing")
|
|
|
|
// Send an empty request to ensure the RPC handler runs the validation
|
|
// func.
|
|
authURLReq1 := structs.ACLOIDCAuthURLRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var authURLResp1 structs.ACLOIDCAuthURLResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLOIDCAuthURLRPCMethod, &authURLReq1, &authURLResp1)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "invalid OIDC auth-url request")
|
|
|
|
// Send a valid request that contains an auth method name that does not
|
|
// exist within state.
|
|
authURLReq2 := structs.ACLOIDCAuthURLRequest{
|
|
AuthMethodName: "test-oidc-auth-method",
|
|
RedirectURI: "http://127.0.0.1:4649/oidc/callback",
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var authURLResp2 structs.ACLOIDCAuthURLResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCAuthURLRPCMethod, &authURLReq2, &authURLResp2)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "auth-method \"test-oidc-auth-method\" not found")
|
|
|
|
// Generate and upsert an ACL auth method for use. Certain values must be
|
|
// taken from the cap OIDC provider just like real world use.
|
|
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
|
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
|
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
|
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
|
mockedAuthMethod.Config.DiscoveryCaPem = []string{oidcTestProvider.CACert()}
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{mockedAuthMethod}))
|
|
|
|
// Make a new request, which contains all valid data and therefore should
|
|
// succeed.
|
|
authURLReq3 := structs.ACLOIDCAuthURLRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var authURLResp3 structs.ACLOIDCAuthURLResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCAuthURLRPCMethod, &authURLReq3, &authURLResp3)
|
|
must.NoError(t, err)
|
|
|
|
// The response URL comes encoded, so decode this and check we have each
|
|
// component we expect.
|
|
escapedURL, err := url.PathUnescape(authURLResp3.AuthURL)
|
|
must.NoError(t, err)
|
|
must.StrContains(t, escapedURL, "/authorize?client_id=mock")
|
|
must.StrContains(t, escapedURL, "&nonce=fsSPuaodKevKfDU3IeXa")
|
|
must.StrContains(t, escapedURL, "&redirect_uri=http://127.0.0.1:4649/oidc/callback")
|
|
must.StrContains(t, escapedURL, "&response_type=code")
|
|
must.StrContains(t, escapedURL, "&scope=openid")
|
|
must.StrContains(t, escapedURL, "&state=st_")
|
|
}
|
|
|
|
func TestACL_OIDCCompleteAuth(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, _, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
oidcTestProvider := capOIDC.StartTestProvider(t)
|
|
defer oidcTestProvider.Stop()
|
|
oidcTestProvider.SetAllowedRedirectURIs([]string{"http://127.0.0.1:4649/oidc/callback"})
|
|
|
|
// Send an empty request to ensure the RPC handler runs the validation
|
|
// func.
|
|
completeAuthReq1 := structs.ACLOIDCCompleteAuthRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp1 structs.ACLLoginResponse
|
|
err := msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq1, &completeAuthResp1)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "invalid OIDC complete-auth request")
|
|
|
|
// Send a request that passes initial validation. The auth method does not
|
|
// exist meaning it will fail.
|
|
completeAuthReq2 := structs.ACLOIDCCompleteAuthRequest{
|
|
AuthMethodName: "test-oidc-auth-method",
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
State: "st_",
|
|
Code: "idontknowthisyet",
|
|
RedirectURI: "http://127.0.0.1:4649/oidc/callback",
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp2 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq2, &completeAuthResp2)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "auth-method \"test-oidc-auth-method\" not found")
|
|
|
|
// Generate and upsert an ACL auth method for use. Certain values must be
|
|
// taken from the cap OIDC provider and these are validated. Others must
|
|
// match data we use later, such as the claims.
|
|
mockedAuthMethod := mock.ACLOIDCAuthMethod()
|
|
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
|
|
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
|
|
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
|
|
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
|
|
mockedAuthMethod.Config.DiscoveryCaPem = []string{oidcTestProvider.CACert()}
|
|
mockedAuthMethod.Config.ClaimMappings = map[string]string{}
|
|
mockedAuthMethod.Config.ListClaimMappings = map[string]string{
|
|
"http://nomad.internal/roles": "roles",
|
|
"http://nomad.internal/policies": "policies",
|
|
}
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{mockedAuthMethod}))
|
|
|
|
// Set our custom data and some expected values, so we can make the RPC and
|
|
// use the test provider.
|
|
oidcTestProvider.SetExpectedAuthNonce("fsSPuaodKevKfDU3IeXa")
|
|
oidcTestProvider.SetExpectedAuthCode("codeABC")
|
|
oidcTestProvider.SetCustomAudience("mock")
|
|
oidcTestProvider.SetExpectedState("st_someweirdstateid")
|
|
oidcTestProvider.SetCustomClaims(map[string]interface{}{
|
|
"azp": "mock",
|
|
"http://nomad.internal/policies": []string{"engineering"},
|
|
"http://nomad.internal/roles": []string{"engineering"},
|
|
})
|
|
|
|
// We should now be able to authenticate, however, we do not have any rule
|
|
// bindings that will match.
|
|
completeAuthReq3 := structs.ACLOIDCCompleteAuthRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
State: "st_",
|
|
Code: "codeABC",
|
|
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp3 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq3, &completeAuthResp3)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "no role or policy bindings matched")
|
|
|
|
// Upsert an ACL policy and role, so that we can reference this within our
|
|
// OIDC claims.
|
|
mockACLPolicy := mock.ACLPolicy()
|
|
must.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{mockACLPolicy}))
|
|
|
|
mockACLRole := mock.ACLRole()
|
|
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: mockACLPolicy.Name}}
|
|
must.NoError(t, testServer.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole}, true))
|
|
|
|
// Generate and upsert two binding rules, so we can test both ACL Policy
|
|
// and Role claim mapping.
|
|
mockBindingRule1 := mock.ACLBindingRule()
|
|
mockBindingRule1.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule1.BindType = structs.ACLBindingRuleBindTypePolicy
|
|
mockBindingRule1.Selector = "engineering in list.policies"
|
|
mockBindingRule1.BindName = mockACLPolicy.Name
|
|
|
|
mockBindingRule2 := mock.ACLBindingRule()
|
|
mockBindingRule2.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule2.BindName = mockACLRole.Name
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
|
40, []*structs.ACLBindingRule{mockBindingRule1, mockBindingRule2}, true))
|
|
|
|
completeAuthReq4 := structs.ACLOIDCCompleteAuthRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
State: "st_someweirdstateid",
|
|
Code: "codeABC",
|
|
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp4 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq4, &completeAuthResp4)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, completeAuthResp4.ACLToken)
|
|
must.Len(t, 1, completeAuthResp4.ACLToken.Policies)
|
|
must.Eq(t, mockACLPolicy.Name, completeAuthResp4.ACLToken.Policies[0])
|
|
must.Len(t, 1, completeAuthResp4.ACLToken.Roles)
|
|
must.Eq(t, mockACLRole.Name, completeAuthResp4.ACLToken.Roles[0].Name)
|
|
must.Eq(t, mockACLRole.ID, completeAuthResp4.ACLToken.Roles[0].ID)
|
|
|
|
// Create a binding rule which generates management tokens. This should
|
|
// override the other rules, giving us a management token when we next
|
|
// log in.
|
|
mockBindingRule3 := mock.ACLBindingRule()
|
|
mockBindingRule3.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule3.BindType = structs.ACLBindingRuleBindTypeManagement
|
|
mockBindingRule3.Selector = "engineering in list.policies"
|
|
mockBindingRule3.BindName = ""
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
|
50, []*structs.ACLBindingRule{mockBindingRule3}, true))
|
|
|
|
completeAuthReq5 := structs.ACLOIDCCompleteAuthRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
ClientNonce: "fsSPuaodKevKfDU3IeXa",
|
|
State: "st_someweirdstateid",
|
|
Code: "codeABC",
|
|
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp5 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &completeAuthReq5, &completeAuthResp5)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, completeAuthResp4.ACLToken)
|
|
must.Len(t, 0, completeAuthResp5.ACLToken.Policies)
|
|
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
|
|
must.Eq(t, structs.ACLManagementToken, completeAuthResp5.ACLToken.Type)
|
|
}
|
|
|
|
func TestACL_Login(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, _, testServerCleanupFn := TestACLServer(t, nil)
|
|
defer testServerCleanupFn()
|
|
codec := rpcClient(t, testServer)
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// create a sample JWT and a pub key for verification
|
|
iat := time.Now().Unix()
|
|
nbf := time.Now().Unix()
|
|
exp := time.Now().Add(time.Hour).Unix()
|
|
testToken, testPubKey, err := mock.SampleJWTokenWithKeys(jwt.MapClaims{
|
|
"http://nomad.internal/policies": []string{"engineering"},
|
|
"http://nomad.internal/roles": []string{"engineering"},
|
|
"iat": iat,
|
|
"nbf": nbf,
|
|
"exp": exp,
|
|
"iss": "nomad test suite",
|
|
"aud": []string{"sales", "engineering"},
|
|
}, nil)
|
|
must.Nil(t, err)
|
|
|
|
// send empty req to test validation
|
|
loginReq1 := structs.ACLLoginRequest{
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp1 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq1, &completeAuthResp1)
|
|
must.ErrorContains(t, err, "missing auth method name")
|
|
must.ErrorContains(t, err, "missing login token")
|
|
|
|
// Send a request that passes initial validation. The auth method does not
|
|
// exist meaning it will fail.
|
|
loginReq2 := structs.ACLLoginRequest{
|
|
AuthMethodName: "test-oidc-auth-method",
|
|
LoginToken: testToken,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp2 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq2, &completeAuthResp2)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "auth-method \"test-oidc-auth-method\" not found")
|
|
|
|
// Generate and upsert a JWT ACL auth method for use.
|
|
mockedAuthMethod := mock.ACLJWTAuthMethod()
|
|
mockedAuthMethod.Config.BoundAudiences = []string{"engineering"}
|
|
mockedAuthMethod.Config.JWTValidationPubKeys = []string{testPubKey}
|
|
mockedAuthMethod.Config.BoundIssuer = []string{"nomad test suite"}
|
|
mockedAuthMethod.Config.ExpirationLeeway = time.Duration(3600)
|
|
mockedAuthMethod.Config.ClockSkewLeeway = time.Duration(3600)
|
|
mockedAuthMethod.Config.ClaimMappings = map[string]string{}
|
|
mockedAuthMethod.Config.ListClaimMappings = map[string]string{
|
|
"http://nomad.internal/roles": "roles",
|
|
"http://nomad.internal/policies": "policies",
|
|
}
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{mockedAuthMethod}))
|
|
|
|
// We should now be able to authenticate, however, we do not have any rule
|
|
// bindings that will match.
|
|
loginReq3 := structs.ACLLoginRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
LoginToken: testToken,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp3 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq3, &completeAuthResp3)
|
|
must.Error(t, err)
|
|
must.ErrorContains(t, err, "400")
|
|
must.ErrorContains(t, err, "no role or policy bindings matched")
|
|
|
|
// Upsert an ACL policy and role, so that we can reference this within our
|
|
// JWT claims.
|
|
mockACLPolicy := mock.ACLPolicy()
|
|
must.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{mockACLPolicy}))
|
|
|
|
mockACLRole := mock.ACLRole()
|
|
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: mockACLPolicy.Name}}
|
|
must.NoError(t, testServer.fsm.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole}, true))
|
|
|
|
// Generate and upsert two binding rules, so we can test both ACL Policy
|
|
// and Role claim mapping.
|
|
mockBindingRule1 := mock.ACLBindingRule()
|
|
mockBindingRule1.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule1.BindType = structs.ACLBindingRuleBindTypePolicy
|
|
mockBindingRule1.Selector = "engineering in list.policies"
|
|
mockBindingRule1.BindName = mockACLPolicy.Name
|
|
|
|
mockBindingRule2 := mock.ACLBindingRule()
|
|
mockBindingRule2.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule2.BindName = mockACLRole.Name
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
|
40, []*structs.ACLBindingRule{mockBindingRule1, mockBindingRule2}, true))
|
|
|
|
loginReq4 := structs.ACLLoginRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
LoginToken: testToken,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp4 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq4, &completeAuthResp4)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, completeAuthResp4.ACLToken)
|
|
must.Len(t, 1, completeAuthResp4.ACLToken.Policies)
|
|
must.Eq(t, mockACLPolicy.Name, completeAuthResp4.ACLToken.Policies[0])
|
|
must.Len(t, 1, completeAuthResp4.ACLToken.Roles)
|
|
must.Eq(t, mockACLRole.Name, completeAuthResp4.ACLToken.Roles[0].Name)
|
|
must.Eq(t, mockACLRole.ID, completeAuthResp4.ACLToken.Roles[0].ID)
|
|
|
|
// Create a binding rule which generates management tokens. This should
|
|
// override the other rules, giving us a management token when we next
|
|
// log in.
|
|
mockBindingRule3 := mock.ACLBindingRule()
|
|
mockBindingRule3.AuthMethod = mockedAuthMethod.Name
|
|
mockBindingRule3.BindType = structs.ACLBindingRuleBindTypeManagement
|
|
mockBindingRule3.Selector = "engineering in list.policies"
|
|
mockBindingRule3.BindName = ""
|
|
|
|
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
|
|
50, []*structs.ACLBindingRule{mockBindingRule3}, true))
|
|
|
|
loginReq5 := structs.ACLLoginRequest{
|
|
AuthMethodName: mockedAuthMethod.Name,
|
|
LoginToken: testToken,
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: DefaultRegion,
|
|
},
|
|
}
|
|
|
|
var completeAuthResp5 structs.ACLLoginResponse
|
|
err = msgpackrpc.CallWithCodec(codec, structs.ACLLoginRPCMethod, &loginReq5, &completeAuthResp5)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, completeAuthResp4.ACLToken)
|
|
must.Len(t, 0, completeAuthResp5.ACLToken.Policies)
|
|
must.Len(t, 0, completeAuthResp5.ACLToken.Roles)
|
|
must.Eq(t, structs.ACLManagementToken, completeAuthResp5.ACLToken.Type)
|
|
}
|