open-nomad/nomad/job_endpoint_oss_test.go
Seth Hoenig 87be8c4c4b consul: correctly check consul acl token namespace when using consul oss
This PR fixes the Nomad Object Namespace <-> Consul ACL Token relationship
check when using Consul OSS (or Consul ENT without namespace support).

Nomad v1.1.0 introduced a regression where Nomad would fail the validation
when submitting Connect jobs and allow_unauthenticated set to true, with
Consul OSS - because it would do the namespace check against the Consul ACL
token assuming the "default" namespace, which does not work because Consul OSS
does not have namespaces.

Instead of making the bad assumption, expand the namespace check to handle
each special case explicitly.

Fixes #10718
2021-06-08 13:55:57 -05:00

333 lines
13 KiB
Go

// +build !ent
package nomad
import (
"testing"
"github.com/hashicorp/go-memdb"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/nomad/command/agent/consul"
"github.com/hashicorp/nomad/helper"
"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/stretchr/testify/require"
)
// TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse asserts that a job
// submission fails allow_unauthenticated is false, and either an invalid or no
// operator Consul token is provided.
func TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse_oss(t *testing.T) {
t.Parallel()
s1, cleanupS1 := TestServer(t, func(c *Config) {
c.NumSchedulers = 0 // Prevent automatic dequeue
c.ConsulConfig.AllowUnauthenticated = helper.BoolToPtr(false)
})
defer cleanupS1()
codec := rpcClient(t, s1)
testutil.WaitForLeader(t, s1.RPC)
newJob := func(namespace string) *structs.Job {
// Create the register request
job := mock.Job()
job.TaskGroups[0].Networks[0].Mode = "bridge"
job.TaskGroups[0].Services = []*structs.Service{
{
Name: "service1", // matches consul.ExamplePolicyID1
PortLabel: "8080",
Connect: &structs.ConsulConnect{
SidecarService: &structs.ConsulSidecarService{},
},
},
}
// For this test we only care about authorizing the connect service
job.TaskGroups[0].Tasks[0].Services = nil
// If testing with a Consul namespace, set it on the group
if namespace != "" {
job.TaskGroups[0].Consul = &structs.Consul{
Namespace: namespace,
}
}
return job
}
newRequest := func(job *structs.Job) *structs.JobRegisterRequest {
return &structs.JobRegisterRequest{
Job: job,
WriteRequest: structs.WriteRequest{
Region: "global",
Namespace: job.Namespace,
},
}
}
noTokenOnJob := func(t *testing.T, job *structs.Job) {
fsmState := s1.State()
ws := memdb.NewWatchSet()
storedJob, err := fsmState.JobByID(ws, job.Namespace, job.ID)
require.NoError(t, err)
require.NotNil(t, storedJob)
require.Empty(t, storedJob.ConsulToken)
}
// Non-sense Consul ACL tokens that should be rejected
missingToken := ""
fakeToken := uuid.Generate()
// Consul ACL tokens in no Consul namespace
ossTokenNoPolicyNoNS := consul.ExampleOperatorTokenID3
ossTokenNoNS := consul.ExampleOperatorTokenID1
// Consul ACL tokens in "default" Consul namespace
entTokenNoPolicyDefaultNS := consul.ExampleOperatorTokenID20
entTokenDefaultNS := consul.ExampleOperatorTokenID21
// Consul ACL tokens in "banana" Consul namespace
entTokenNoPolicyBananaNS := consul.ExampleOperatorTokenID10
entTokenBananaNS := consul.ExampleOperatorTokenID11
t.Run("group consul namespace unset", func(t *testing.T) {
// When the group namespace is unset (which is always the case with
// Nomad OSS), Consul tokens with no namespace or are in the "default"
// namespace should be accepted (assuming a sufficient service policy).
namespace := ""
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
t.Run("group consul namespace banana", func(t *testing.T) {
// Nomad OSS does not respect setting the consul namespace field on the group,
// and for backwards compatibility accepts tokens in the "default" namespace
// for groups with no namespace set. The net result is setting the group namespace
// to something like "banana" and using a token in "default" namespace will
// be accepted in Nomad OSS (assuming sufficient service write policy).
//
// Using a Consul token in the non-"default" namespace will always fail in
// Nomad OSS, again because the group namespace is ignored.
namespace := "banana"
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
// Consul token in custom namespace will always fail in nomad oss
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
t.Run("group consul namespace default", func(t *testing.T) {
// Nomad OSS ignores the group consul namespace, and setting it as default
// should effectively be the same as leaving it unset.
namespace := "default"
t.Run("no token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = missingToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
})
t.Run("unknown token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = fakeToken
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
})
t.Run("unauthorized oss token provided", func(t *testing.T) {
request := newRequest(newJob(namespace))
request.Job.ConsulToken = ossTokenNoPolicyNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized oss token provided", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = ossTokenNoNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
})
t.Run("authorized token in default namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenDefaultNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.NoError(t, err)
noTokenOnJob(t, job)
})
t.Run("unauthorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenNoPolicyBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
t.Run("authorized token in banana namespace", func(t *testing.T) {
job := newJob(namespace)
request := newRequest(job)
request.Job.ConsulToken = entTokenBananaNS
var response structs.JobRegisterResponse
err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
})
})
}