055434cca9
Implement a metric for RPC requests with labels on the identity, so that administrators can monitor the source of requests within the cluster. This changeset demonstrates the change with the new `ACL.WhoAmI` RPC, and we'll wire up the remaining RPCs once we've threaded the new pre-forwarding authentication through the all. Note that metrics are measured after we forward but before we return any authentication error. This ensures that we only emit metrics on the server that actually serves the request. We'll perform rate limiting at the same place. Includes telemetry configuration to omit identity labels.
784 lines
23 KiB
Go
784 lines
23 KiB
Go
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"testing"
|
|
"time"
|
|
|
|
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
|
"github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/ci"
|
|
"github.com/hashicorp/nomad/helper/pointer"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/nomad/mock"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"github.com/hashicorp/nomad/nomad/structs/config"
|
|
"github.com/hashicorp/nomad/testutil"
|
|
"github.com/shoenig/test"
|
|
"github.com/shoenig/test/must"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAuthenticate_mTLS(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
// Set up a cluster with mTLS and ACLs
|
|
|
|
dir := t.TempDir()
|
|
|
|
tlsCfg := &config.TLSConfig{
|
|
EnableHTTP: true,
|
|
EnableRPC: true,
|
|
VerifyServerHostname: true,
|
|
CAFile: "../helper/tlsutil/testdata/ca.pem",
|
|
CertFile: "../helper/tlsutil/testdata/nomad-foo.pem",
|
|
KeyFile: "../helper/tlsutil/testdata/nomad-foo-key.pem",
|
|
}
|
|
clientTLSCfg := tlsCfg.Copy()
|
|
clientTLSCfg.CertFile = "../helper/tlsutil/testdata/nomad-foo-client.pem"
|
|
clientTLSCfg.KeyFile = "../helper/tlsutil/testdata/nomad-foo-client-key.pem"
|
|
|
|
setCfg := func(name string, bootstrapExpect int) func(*Config) {
|
|
return func(c *Config) {
|
|
c.Region = "regionFoo"
|
|
c.AuthoritativeRegion = "regionFoo"
|
|
c.ACLEnabled = true
|
|
c.BootstrapExpect = bootstrapExpect
|
|
c.NumSchedulers = 0
|
|
c.DevMode = false
|
|
c.DataDir = path.Join(dir, name)
|
|
c.TLSConfig = tlsCfg
|
|
}
|
|
}
|
|
|
|
leader, cleanupLeader := TestServer(t, setCfg("node1", 1))
|
|
defer cleanupLeader()
|
|
testutil.WaitForLeader(t, leader.RPC)
|
|
|
|
follower, cleanupFollower := TestServer(t, setCfg("node2", 0))
|
|
defer cleanupFollower()
|
|
|
|
TestJoin(t, leader, follower)
|
|
testutil.WaitForLeader(t, leader.RPC)
|
|
|
|
testutil.Wait(t, func() (bool, error) {
|
|
keyset, err := follower.encrypter.activeKeySet()
|
|
return keyset != nil, err
|
|
})
|
|
|
|
rootToken := uuid.Generate()
|
|
var bootstrapResp *structs.ACLTokenUpsertResponse
|
|
|
|
codec := rpcClientWithTLS(t, follower, tlsCfg)
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec,
|
|
"ACL.Bootstrap", &structs.ACLTokenBootstrapRequest{
|
|
BootstrapSecret: rootToken,
|
|
WriteRequest: structs.WriteRequest{Region: "regionFoo"},
|
|
}, &bootstrapResp))
|
|
must.NotNil(t, bootstrapResp)
|
|
must.Len(t, 1, bootstrapResp.Tokens)
|
|
rootAccessor := bootstrapResp.Tokens[0].AccessorID
|
|
|
|
// create some ACL tokens directly into raft so we can bypass RPC validation
|
|
// around expiration times
|
|
|
|
token1 := mock.ACLToken()
|
|
token2 := mock.ACLToken()
|
|
expireTime := time.Now().Add(time.Second * -10)
|
|
token2.ExpirationTime = &expireTime
|
|
|
|
_, _, err := leader.raftApply(structs.ACLTokenUpsertRequestType,
|
|
&structs.ACLTokenUpsertRequest{Tokens: []*structs.ACLToken{token1, token2}})
|
|
must.NoError(t, err)
|
|
|
|
// create a node so we can test client RPCs
|
|
|
|
node := mock.Node()
|
|
nodeRegisterReq := &structs.NodeRegisterRequest{
|
|
Node: node,
|
|
WriteRequest: structs.WriteRequest{Region: "regionFoo"},
|
|
}
|
|
var nodeRegisterResp structs.NodeUpdateResponse
|
|
|
|
must.NoError(t, msgpackrpc.CallWithCodec(codec,
|
|
"Node.Register", nodeRegisterReq, &nodeRegisterResp))
|
|
must.NotNil(t, bootstrapResp)
|
|
|
|
// create some allocations so we can test WorkloadIdentity claims. we'll
|
|
// create directly into raft so we can bypass RPC validation and the whole
|
|
// eval, plan, etc. workflow.
|
|
job := mock.Job()
|
|
|
|
_, _, err = leader.raftApply(structs.JobRegisterRequestType,
|
|
&structs.JobRegisterRequest{Job: job})
|
|
must.NoError(t, err)
|
|
|
|
alloc1 := mock.Alloc()
|
|
alloc1.NodeID = node.ID
|
|
alloc1.ClientStatus = structs.AllocClientStatusFailed
|
|
alloc1.Job = job
|
|
alloc1.JobID = job.ID
|
|
|
|
alloc2 := mock.Alloc()
|
|
alloc2.NodeID = node.ID
|
|
alloc2.Job = job
|
|
alloc2.JobID = job.ID
|
|
alloc2.ClientStatus = structs.AllocClientStatusRunning
|
|
|
|
claims1 := alloc1.ToTaskIdentityClaims(nil, "web")
|
|
claims1Token, _, err := leader.encrypter.SignClaims(claims1)
|
|
must.NoError(t, err, must.Sprint("could not sign claims"))
|
|
|
|
claims2 := alloc2.ToTaskIdentityClaims(nil, "web")
|
|
claims2Token, _, err := leader.encrypter.SignClaims(claims2)
|
|
must.NoError(t, err, must.Sprint("could not sign claims"))
|
|
|
|
planReq := &structs.ApplyPlanResultsRequest{
|
|
AllocUpdateRequest: structs.AllocUpdateRequest{
|
|
Alloc: []*structs.Allocation{alloc1, alloc2},
|
|
Job: job,
|
|
},
|
|
}
|
|
_, _, err = leader.raftApply(structs.ApplyPlanResultsRequestType, planReq)
|
|
must.NoError(t, err)
|
|
|
|
testutil.WaitForResult(func() (bool, error) {
|
|
store := follower.fsm.State()
|
|
alloc, err := store.AllocByID(nil, alloc1.ID)
|
|
return alloc != nil, err
|
|
}, func(err error) {
|
|
t.Fatalf("alloc was not replicated via raft: %v", err) // should never happen
|
|
})
|
|
|
|
testCases := []struct {
|
|
name string
|
|
tlsCfg *config.TLSConfig
|
|
stale bool
|
|
testToken string
|
|
expectAccessor string
|
|
expectClientID string
|
|
expectAllocID string
|
|
expectTLSName string
|
|
expectIP string
|
|
expectErr string
|
|
expectIDKey string
|
|
sendFromPeer *Server
|
|
}{
|
|
{
|
|
name: "root token",
|
|
tlsCfg: clientTLSCfg, // TODO: this is a mixed use cert
|
|
testToken: rootToken,
|
|
expectAccessor: rootAccessor,
|
|
expectIDKey: fmt.Sprintf("token:%s", rootAccessor),
|
|
},
|
|
{
|
|
name: "from peer to leader without token", // ex. Eval.Dequeue
|
|
tlsCfg: tlsCfg,
|
|
expectTLSName: "regionFoo.nomad",
|
|
expectAccessor: "anonymous",
|
|
expectIP: follower.GetConfig().RPCAddr.IP.String(),
|
|
sendFromPeer: follower,
|
|
expectIDKey: "token:anonymous",
|
|
},
|
|
{
|
|
// note: this test is somewhat bogus because under test all the
|
|
// servers share the same IP address with the RPC client
|
|
name: "anonymous forwarded from peer to leader",
|
|
tlsCfg: tlsCfg,
|
|
expectAccessor: "anonymous",
|
|
expectTLSName: "regionFoo.nomad",
|
|
expectIP: "127.0.0.1",
|
|
expectIDKey: "token:anonymous",
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: uuid.Generate(),
|
|
expectTLSName: "regionFoo.nomad",
|
|
expectIP: follower.GetConfig().RPCAddr.IP.String(),
|
|
expectIDKey: "regionFoo.nomad:127.0.0.1",
|
|
},
|
|
{
|
|
name: "from peer to leader with leader ACL", // ex. core job GC
|
|
tlsCfg: tlsCfg,
|
|
testToken: leader.getLeaderAcl(),
|
|
expectTLSName: "regionFoo.nomad",
|
|
expectAccessor: "leader",
|
|
expectIP: follower.GetConfig().RPCAddr.IP.String(),
|
|
sendFromPeer: follower,
|
|
expectIDKey: "token:leader",
|
|
},
|
|
{
|
|
name: "from client", // ex. Node.GetAllocs
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: node.SecretID,
|
|
expectClientID: node.ID,
|
|
expectIDKey: fmt.Sprintf("client:%s", node.ID),
|
|
},
|
|
{
|
|
name: "from failed workload", // ex. Variables.List
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: claims1Token,
|
|
expectErr: "rpc error: allocation is terminal",
|
|
},
|
|
{
|
|
name: "from running workload", // ex. Variables.List
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: claims2Token,
|
|
expectAllocID: alloc2.ID,
|
|
expectIDKey: fmt.Sprintf("alloc:%s", alloc2.ID),
|
|
},
|
|
{
|
|
name: "valid user token",
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: token1.SecretID,
|
|
expectAccessor: token1.AccessorID,
|
|
expectIDKey: fmt.Sprintf("token:%s", token1.AccessorID),
|
|
},
|
|
{
|
|
name: "expired user token",
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: token2.SecretID,
|
|
expectErr: "rpc error: ACL token expired",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
req := &structs.GenericRequest{
|
|
QueryOptions: structs.QueryOptions{
|
|
Region: "regionFoo",
|
|
AllowStale: tc.stale,
|
|
AuthToken: tc.testToken,
|
|
},
|
|
}
|
|
var resp structs.ACLWhoAmIResponse
|
|
var err error
|
|
|
|
if tc.sendFromPeer != nil {
|
|
aclEndpoint := NewACLEndpoint(tc.sendFromPeer, nil)
|
|
err = aclEndpoint.WhoAmI(req, &resp)
|
|
} else {
|
|
err = msgpackrpc.CallWithCodec(codec, "ACL.WhoAmI", req, &resp)
|
|
}
|
|
|
|
if tc.expectErr != "" {
|
|
must.EqError(t, err, tc.expectErr)
|
|
return
|
|
}
|
|
|
|
must.NoError(t, err)
|
|
must.NotNil(t, resp)
|
|
must.NotNil(t, resp.Identity)
|
|
|
|
if tc.expectIDKey != "" {
|
|
must.Eq(t, tc.expectIDKey, resp.Identity.String(),
|
|
must.Sprintf("expected identity key for metrics to match"))
|
|
}
|
|
|
|
if tc.expectAccessor != "" {
|
|
must.NotNil(t, resp.Identity.ACLToken, must.Sprint("expected ACL token"))
|
|
test.Eq(t, tc.expectAccessor, resp.Identity.ACLToken.AccessorID,
|
|
test.Sprint("expected ACL token accessor ID"))
|
|
}
|
|
|
|
test.Eq(t, tc.expectClientID, resp.Identity.ClientID,
|
|
test.Sprint("expected client ID"))
|
|
|
|
if tc.expectAllocID != "" {
|
|
must.NotNil(t, resp.Identity.Claims, must.Sprint("expected claims"))
|
|
test.Eq(t, tc.expectAllocID, resp.Identity.Claims.AllocationID,
|
|
test.Sprint("expected workload identity"))
|
|
}
|
|
|
|
test.Eq(t, tc.expectTLSName, resp.Identity.TLSName, test.Sprint("expected TLS name"))
|
|
|
|
if tc.expectIP == "" {
|
|
test.Nil(t, resp.Identity.RemoteIP, test.Sprint("expected no remote IP"))
|
|
} else {
|
|
test.Eq(t, tc.expectIP, resp.Identity.RemoteIP.String())
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveACLToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
testFn func()
|
|
}{
|
|
{
|
|
name: "leader token",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Check the leader ACL token is correctly set.
|
|
leaderACL := testServer.getLeaderAcl()
|
|
require.NotEmpty(t, leaderACL)
|
|
|
|
// Resolve the token and ensure it's a management token.
|
|
aclResp, err := testServer.ResolveToken(leaderACL)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.True(t, aclResp.IsManagement())
|
|
},
|
|
},
|
|
{
|
|
name: "anonymous token",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Call the function with an empty input secret ID which is
|
|
// classed as representing anonymous access in clusters with
|
|
// ACLs enabled.
|
|
aclResp, err := testServer.ResolveToken("")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.False(t, aclResp.IsManagement())
|
|
},
|
|
},
|
|
{
|
|
name: "token not found",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Call the function with randomly generated secret ID which
|
|
// does not exist within state.
|
|
aclResp, err := testServer.ResolveToken(uuid.Generate())
|
|
require.Equal(t, structs.ErrTokenNotFound, err)
|
|
require.Nil(t, aclResp)
|
|
},
|
|
},
|
|
{
|
|
name: "token expired",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create a mock token with an expiration time long in the
|
|
// past, and upsert.
|
|
token := mock.ACLToken()
|
|
token.ExpirationTime = pointer.Of(time.Date(
|
|
1970, time.January, 1, 0, 0, 0, 0, time.UTC))
|
|
|
|
err := testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
|
require.NoError(t, err)
|
|
|
|
// Perform the function call which should result in finding the
|
|
// token has expired.
|
|
aclResp, err := testServer.ResolveToken(uuid.Generate())
|
|
require.Equal(t, structs.ErrTokenNotFound, err)
|
|
require.Nil(t, aclResp)
|
|
},
|
|
},
|
|
{
|
|
name: "management token",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Generate a management token and upsert this.
|
|
managementToken := mock.ACLToken()
|
|
managementToken.Type = structs.ACLManagementToken
|
|
managementToken.Policies = nil
|
|
|
|
err := testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{managementToken})
|
|
require.NoError(t, err)
|
|
|
|
// Resolve the token and check that we received a management
|
|
// ACL.
|
|
aclResp, err := testServer.ResolveToken(managementToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.True(t, aclResp.IsManagement())
|
|
require.Equal(t, acl.ManagementACL, aclResp)
|
|
},
|
|
},
|
|
{
|
|
name: "client token with policies only",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Generate a client token with associated policies and upsert
|
|
// these.
|
|
policy1 := mock.ACLPolicy()
|
|
policy2 := mock.ACLPolicy()
|
|
err := testServer.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
|
|
|
clientToken := mock.ACLToken()
|
|
clientToken.Policies = []string{policy1.Name, policy2.Name}
|
|
err = testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLToken{clientToken})
|
|
require.NoError(t, err)
|
|
|
|
// Resolve the token and check that we received a client
|
|
// ACL with appropriate permissions.
|
|
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.False(t, aclResp.IsManagement())
|
|
|
|
allowed := aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs)
|
|
require.True(t, allowed)
|
|
allowed = aclResp.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs)
|
|
require.False(t, allowed)
|
|
|
|
// Resolve the same token again and ensure we get the same
|
|
// result.
|
|
aclResp2, err := testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp2)
|
|
require.Equal(t, aclResp, aclResp2)
|
|
|
|
// Bust the cache by upserting the policy
|
|
err = testServer.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLPolicy{policy1})
|
|
require.Nil(t, err)
|
|
|
|
// Resolve the same token again, should get different value
|
|
aclResp3, err := testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp3)
|
|
require.NotEqual(t, aclResp2, aclResp3)
|
|
},
|
|
},
|
|
{
|
|
name: "client token with roles only",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Create a client token that only has a link to a role.
|
|
policy1 := mock.ACLPolicy()
|
|
policy2 := mock.ACLPolicy()
|
|
err := testServer.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
|
|
|
aclRole := mock.ACLRole()
|
|
aclRole.Policies = []*structs.ACLRolePolicyLink{
|
|
{Name: policy1.Name},
|
|
{Name: policy2.Name},
|
|
}
|
|
err = testServer.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{aclRole}, false)
|
|
require.NoError(t, err)
|
|
|
|
clientToken := mock.ACLToken()
|
|
clientToken.Policies = []string{}
|
|
clientToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRole.ID}}
|
|
err = testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{clientToken})
|
|
require.NoError(t, err)
|
|
|
|
// Resolve the token and check that we received a client
|
|
// ACL with appropriate permissions.
|
|
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.False(t, aclResp.IsManagement())
|
|
|
|
allowed := aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs)
|
|
require.True(t, allowed)
|
|
allowed = aclResp.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs)
|
|
require.False(t, allowed)
|
|
|
|
// Remove the policies from the ACL role and ensure the resolution
|
|
// permissions are updated.
|
|
aclRole.Policies = []*structs.ACLRolePolicyLink{}
|
|
err = testServer.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 40, []*structs.ACLRole{aclRole}, false)
|
|
require.NoError(t, err)
|
|
|
|
aclResp, err = testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.False(t, aclResp.IsManagement())
|
|
require.False(t, aclResp.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs))
|
|
},
|
|
},
|
|
{
|
|
name: "client with roles and policies",
|
|
testFn: func() {
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
// Generate two policies, each with a different namespace
|
|
// permission set.
|
|
policy1 := &structs.ACLPolicy{
|
|
Name: "policy-" + uuid.Generate(),
|
|
Rules: `namespace "platform" { policy = "write"}`,
|
|
CreateIndex: 10,
|
|
ModifyIndex: 10,
|
|
}
|
|
policy1.SetHash()
|
|
policy2 := &structs.ACLPolicy{
|
|
Name: "policy-" + uuid.Generate(),
|
|
Rules: `namespace "web" { policy = "write"}`,
|
|
CreateIndex: 10,
|
|
ModifyIndex: 10,
|
|
}
|
|
policy2.SetHash()
|
|
|
|
err := testServer.State().UpsertACLPolicies(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2})
|
|
require.NoError(t, err)
|
|
|
|
// Create a role which references the policy that has access to
|
|
// the web namespace.
|
|
aclRole := mock.ACLRole()
|
|
aclRole.Policies = []*structs.ACLRolePolicyLink{{Name: policy2.Name}}
|
|
err = testServer.State().UpsertACLRoles(
|
|
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{aclRole}, false)
|
|
require.NoError(t, err)
|
|
|
|
// Create a token which references the policy and role.
|
|
clientToken := mock.ACLToken()
|
|
clientToken.Policies = []string{policy1.Name}
|
|
clientToken.Roles = []*structs.ACLTokenRoleLink{{ID: aclRole.ID}}
|
|
err = testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 30, []*structs.ACLToken{clientToken})
|
|
require.NoError(t, err)
|
|
|
|
// Resolve the token and check that we received a client
|
|
// ACL with appropriate permissions.
|
|
aclResp, err := testServer.ResolveToken(clientToken.SecretID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, aclResp)
|
|
require.False(t, aclResp.IsManagement())
|
|
|
|
allowed := aclResp.AllowNamespaceOperation("platform", acl.NamespaceCapabilityListJobs)
|
|
require.True(t, allowed)
|
|
allowed = aclResp.AllowNamespaceOperation("web", acl.NamespaceCapabilityListJobs)
|
|
require.True(t, allowed)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.testFn()
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveSecretToken(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
testServer, _, testServerCleanup := TestACLServer(t, nil)
|
|
defer testServerCleanup()
|
|
testutil.WaitForLeader(t, testServer.RPC)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
testFn func(testServer *Server)
|
|
}{
|
|
{
|
|
name: "valid token",
|
|
testFn: func(testServer *Server) {
|
|
|
|
// Generate and upsert a token.
|
|
token := mock.ACLToken()
|
|
err := testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
|
require.NoError(t, err)
|
|
|
|
// Attempt to look up the token and perform checks.
|
|
tokenResp, err := testServer.ResolveSecretToken(token.SecretID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, tokenResp)
|
|
require.Equal(t, token, tokenResp)
|
|
},
|
|
},
|
|
{
|
|
name: "anonymous token",
|
|
testFn: func(testServer *Server) {
|
|
|
|
// Call the function with an empty input secret ID which is
|
|
// classed as representing anonymous access in clusters with
|
|
// ACLs enabled.
|
|
tokenResp, err := testServer.ResolveSecretToken("")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, tokenResp)
|
|
require.Equal(t, structs.AnonymousACLToken, tokenResp)
|
|
},
|
|
},
|
|
{
|
|
name: "token not found",
|
|
testFn: func(testServer *Server) {
|
|
|
|
// Call the function with randomly generated secret ID which
|
|
// does not exist within state.
|
|
tokenResp, err := testServer.ResolveSecretToken(uuid.Generate())
|
|
require.Equal(t, structs.ErrTokenNotFound, err)
|
|
require.Nil(t, tokenResp)
|
|
},
|
|
},
|
|
{
|
|
name: "token expired",
|
|
testFn: func(testServer *Server) {
|
|
|
|
// Create a mock token with an expiration time long in the
|
|
// past, and upsert.
|
|
token := mock.ACLToken()
|
|
token.ExpirationTime = pointer.Of(time.Date(
|
|
1970, time.January, 1, 0, 0, 0, 0, time.UTC))
|
|
|
|
err := testServer.State().UpsertACLTokens(
|
|
structs.MsgTypeTestSetup, 10, []*structs.ACLToken{token})
|
|
require.NoError(t, err)
|
|
|
|
// Perform the function call which should result in finding the
|
|
// token has expired.
|
|
tokenResp, err := testServer.ResolveSecretToken(uuid.Generate())
|
|
require.Equal(t, structs.ErrTokenNotFound, err)
|
|
require.Nil(t, tokenResp)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
tc.testFn(testServer)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveClaims(t *testing.T) {
|
|
ci.Parallel(t)
|
|
|
|
srv, _, cleanup := TestACLServer(t, nil)
|
|
defer cleanup()
|
|
|
|
store := srv.fsm.State()
|
|
index := uint64(100)
|
|
|
|
alloc := mock.Alloc()
|
|
|
|
claims := &structs.IdentityClaims{
|
|
Namespace: alloc.Namespace,
|
|
JobID: alloc.Job.ID,
|
|
AllocationID: alloc.ID,
|
|
TaskName: alloc.Job.TaskGroups[0].Tasks[0].Name,
|
|
}
|
|
|
|
// unrelated policy
|
|
policy0 := mock.ACLPolicy()
|
|
|
|
// policy for job
|
|
policy1 := mock.ACLPolicy()
|
|
policy1.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: claims.JobID,
|
|
}
|
|
|
|
// policy for job and group
|
|
policy2 := mock.ACLPolicy()
|
|
policy2.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: claims.JobID,
|
|
Group: alloc.Job.TaskGroups[0].Name,
|
|
}
|
|
|
|
// policy for job and group and task
|
|
policy3 := mock.ACLPolicy()
|
|
policy3.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: claims.JobID,
|
|
Group: alloc.Job.TaskGroups[0].Name,
|
|
Task: claims.TaskName,
|
|
}
|
|
|
|
// policy for job and group but different task
|
|
policy4 := mock.ACLPolicy()
|
|
policy4.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: claims.JobID,
|
|
Group: alloc.Job.TaskGroups[0].Name,
|
|
Task: "another",
|
|
}
|
|
|
|
// policy for job but different group
|
|
policy5 := mock.ACLPolicy()
|
|
policy5.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: claims.JobID,
|
|
Group: "another",
|
|
}
|
|
|
|
// policy for same namespace but different job
|
|
policy6 := mock.ACLPolicy()
|
|
policy6.JobACL = &structs.JobACL{
|
|
Namespace: claims.Namespace,
|
|
JobID: "another",
|
|
}
|
|
|
|
// policy for same job in different namespace
|
|
policy7 := mock.ACLPolicy()
|
|
policy7.JobACL = &structs.JobACL{
|
|
Namespace: "another",
|
|
JobID: claims.JobID,
|
|
}
|
|
|
|
index++
|
|
err := store.UpsertACLPolicies(structs.MsgTypeTestSetup, index, []*structs.ACLPolicy{
|
|
policy0, policy1, policy2, policy3, policy4, policy5, policy6, policy7})
|
|
must.NoError(t, err)
|
|
|
|
aclObj, err := srv.ResolveClaims(claims)
|
|
must.Nil(t, aclObj)
|
|
must.EqError(t, err, "allocation does not exist")
|
|
|
|
// upsert the allocation
|
|
index++
|
|
err = store.UpsertAllocs(structs.MsgTypeTestSetup, index, []*structs.Allocation{alloc})
|
|
must.NoError(t, err)
|
|
|
|
aclObj, err = srv.ResolveClaims(claims)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, aclObj)
|
|
|
|
// Check that the ACL object looks reasonable
|
|
must.False(t, aclObj.IsManagement())
|
|
must.True(t, aclObj.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs))
|
|
must.False(t, aclObj.AllowNamespaceOperation("other", acl.NamespaceCapabilityListJobs))
|
|
|
|
// Resolve the same claim again, should get cache value
|
|
aclObj2, err := srv.ResolveClaims(claims)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, aclObj)
|
|
must.Eq(t, aclObj, aclObj2, must.Sprintf("expected cached value"))
|
|
|
|
policies, err := srv.resolvePoliciesForClaims(claims)
|
|
must.NoError(t, err)
|
|
must.Len(t, 3, policies)
|
|
must.SliceContainsAll(t, policies, []*structs.ACLPolicy{policy1, policy2, policy3})
|
|
}
|