87be8c4c4b
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
333 lines
13 KiB
Go
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"`)
|
|
})
|
|
})
|
|
}
|