801 lines
24 KiB
Go
801 lines
24 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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/nomad-agent-ca.pem",
|
|
CertFile: "../helper/tlsutil/testdata/regionFoo-server-nomad.pem",
|
|
KeyFile: "../helper/tlsutil/testdata/regionFoo-server-nomad-key.pem",
|
|
}
|
|
clientTLSCfg := tlsCfg.Copy()
|
|
clientTLSCfg.CertFile = "../helper/tlsutil/testdata/regionFoo-client-nomad.pem"
|
|
clientTLSCfg.KeyFile = "../helper/tlsutil/testdata/regionFoo-client-nomad-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: "server.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: "server.regionFoo.nomad",
|
|
expectIP: "127.0.0.1",
|
|
expectIDKey: "token:anonymous",
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
tlsCfg: clientTLSCfg,
|
|
testToken: uuid.Generate(),
|
|
expectTLSName: "server.regionFoo.nomad",
|
|
expectIP: follower.GetConfig().RPCAddr.IP.String(),
|
|
expectIDKey: "server.regionFoo.nomad:127.0.0.1",
|
|
expectErr: "rpc error: Permission denied",
|
|
},
|
|
{
|
|
name: "from peer to leader with leader ACL", // ex. core job GC
|
|
tlsCfg: tlsCfg,
|
|
testToken: leader.getLeaderAcl(),
|
|
expectTLSName: "server.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 client missing secret", // ex. Node.Register
|
|
tlsCfg: clientTLSCfg,
|
|
expectAccessor: "anonymous",
|
|
expectTLSName: "server.regionFoo.nomad",
|
|
expectIP: follower.GetConfig().RPCAddr.IP.String(),
|
|
},
|
|
{
|
|
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,
|
|
}
|
|
|
|
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)
|
|
|
|
// Resolve claims and check we that the ACL object without policies provides no access
|
|
aclObj, err = srv.ResolveClaims(claims)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, aclObj)
|
|
must.False(t, aclObj.AllowNamespaceOperation("default", acl.NamespaceCapabilityListJobs))
|
|
|
|
// Add the policies
|
|
index++
|
|
err = store.UpsertACLPolicies(structs.MsgTypeTestSetup, index, []*structs.ACLPolicy{
|
|
policy0, policy1, policy2, policy3, policy4, policy5, policy6, policy7})
|
|
must.NoError(t, err)
|
|
|
|
// Re-resolve and check that the resulting ACL looks reasonable
|
|
aclObj, err = srv.ResolveClaims(claims)
|
|
must.NoError(t, err)
|
|
must.NotNil(t, aclObj)
|
|
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})
|
|
}
|