github.com/hernad/nomad@v1.6.112/nomad/job_endpoint_oss_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  //go:build !ent
     5  // +build !ent
     6  
     7  package nomad
     8  
     9  import (
    10  	"testing"
    11  
    12  	"github.com/hashicorp/go-memdb"
    13  	msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
    14  	"github.com/hernad/nomad/ci"
    15  	"github.com/hernad/nomad/command/agent/consul"
    16  	"github.com/hernad/nomad/helper/pointer"
    17  	"github.com/hernad/nomad/helper/uuid"
    18  	"github.com/hernad/nomad/nomad/mock"
    19  	"github.com/hernad/nomad/nomad/structs"
    20  	"github.com/hernad/nomad/testutil"
    21  	"github.com/shoenig/test/must"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  // TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse asserts that a job
    26  // submission fails allow_unauthenticated is false, and either an invalid or no
    27  // operator Consul token is provided.
    28  func TestJobEndpoint_Register_Connect_AllowUnauthenticatedFalse_oss(t *testing.T) {
    29  	ci.Parallel(t)
    30  
    31  	s1, cleanupS1 := TestServer(t, func(c *Config) {
    32  		c.NumSchedulers = 0 // Prevent automatic dequeue
    33  		c.ConsulConfig.AllowUnauthenticated = pointer.Of(false)
    34  	})
    35  	defer cleanupS1()
    36  	codec := rpcClient(t, s1)
    37  	testutil.WaitForLeader(t, s1.RPC)
    38  
    39  	newJob := func(namespace string) *structs.Job {
    40  		// Create the register request
    41  		job := mock.Job()
    42  		job.TaskGroups[0].Networks[0].Mode = "bridge"
    43  		job.TaskGroups[0].Services = []*structs.Service{
    44  			{
    45  				Name:      "service1", // matches consul.ExamplePolicyID1
    46  				PortLabel: "8080",
    47  				Connect: &structs.ConsulConnect{
    48  					SidecarService: &structs.ConsulSidecarService{},
    49  				},
    50  			},
    51  		}
    52  		// For this test we only care about authorizing the connect service
    53  		job.TaskGroups[0].Tasks[0].Services = nil
    54  
    55  		// If testing with a Consul namespace, set it on the group
    56  		if namespace != "" {
    57  			job.TaskGroups[0].Consul = &structs.Consul{
    58  				Namespace: namespace,
    59  			}
    60  		}
    61  		return job
    62  	}
    63  
    64  	newRequest := func(job *structs.Job) *structs.JobRegisterRequest {
    65  		return &structs.JobRegisterRequest{
    66  			Job: job,
    67  			WriteRequest: structs.WriteRequest{
    68  				Region:    "global",
    69  				Namespace: job.Namespace,
    70  			},
    71  		}
    72  	}
    73  
    74  	noTokenOnJob := func(t *testing.T, job *structs.Job) {
    75  		fsmState := s1.State()
    76  		ws := memdb.NewWatchSet()
    77  		storedJob, err := fsmState.JobByID(ws, job.Namespace, job.ID)
    78  		require.NoError(t, err)
    79  		require.NotNil(t, storedJob)
    80  		require.Empty(t, storedJob.ConsulToken)
    81  	}
    82  
    83  	// Non-sense Consul ACL tokens that should be rejected
    84  	missingToken := ""
    85  	fakeToken := uuid.Generate()
    86  
    87  	// Consul ACL tokens in no Consul namespace
    88  	ossTokenNoPolicyNoNS := consul.ExampleOperatorTokenID3
    89  	ossTokenNoNS := consul.ExampleOperatorTokenID1
    90  
    91  	// Consul ACL tokens in "default" Consul namespace
    92  	entTokenNoPolicyDefaultNS := consul.ExampleOperatorTokenID20
    93  	entTokenDefaultNS := consul.ExampleOperatorTokenID21
    94  
    95  	// Consul ACL tokens in "banana" Consul namespace
    96  	entTokenNoPolicyBananaNS := consul.ExampleOperatorTokenID10
    97  	entTokenBananaNS := consul.ExampleOperatorTokenID11
    98  
    99  	t.Run("group consul namespace unset", func(t *testing.T) {
   100  		// When the group namespace is unset (which is always the case with
   101  		// Nomad OSS), Consul tokens with no namespace or are in the "default"
   102  		// namespace should be accepted (assuming a sufficient service policy).
   103  		namespace := ""
   104  
   105  		t.Run("no token provided", func(t *testing.T) {
   106  			request := newRequest(newJob(namespace))
   107  			request.Job.ConsulToken = missingToken
   108  			var response structs.JobRegisterResponse
   109  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   110  			require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
   111  		})
   112  
   113  		t.Run("unknown token provided", func(t *testing.T) {
   114  			request := newRequest(newJob(namespace))
   115  			request.Job.ConsulToken = fakeToken
   116  			var response structs.JobRegisterResponse
   117  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   118  			require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
   119  		})
   120  
   121  		t.Run("unauthorized oss token provided", func(t *testing.T) {
   122  			request := newRequest(newJob(namespace))
   123  			request.Job.ConsulToken = ossTokenNoPolicyNoNS
   124  			var response structs.JobRegisterResponse
   125  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   126  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   127  		})
   128  
   129  		t.Run("authorized oss token provided", func(t *testing.T) {
   130  			job := newJob(namespace)
   131  			request := newRequest(job)
   132  			request.Job.ConsulToken = ossTokenNoNS
   133  			var response structs.JobRegisterResponse
   134  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   135  			require.NoError(t, err)
   136  			noTokenOnJob(t, job)
   137  		})
   138  
   139  		t.Run("unauthorized token in default namespace", func(t *testing.T) {
   140  			job := newJob(namespace)
   141  			request := newRequest(job)
   142  			request.Job.ConsulToken = entTokenNoPolicyDefaultNS
   143  			var response structs.JobRegisterResponse
   144  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   145  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   146  		})
   147  
   148  		t.Run("authorized token in default namespace", func(t *testing.T) {
   149  			job := newJob(namespace)
   150  			request := newRequest(job)
   151  			request.Job.ConsulToken = entTokenDefaultNS
   152  			var response structs.JobRegisterResponse
   153  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   154  			require.NoError(t, err)
   155  			noTokenOnJob(t, job)
   156  		})
   157  
   158  		t.Run("unauthorized token in banana namespace", func(t *testing.T) {
   159  			job := newJob(namespace)
   160  			request := newRequest(job)
   161  			request.Job.ConsulToken = entTokenNoPolicyBananaNS
   162  			var response structs.JobRegisterResponse
   163  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   164  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   165  		})
   166  
   167  		t.Run("authorized token in banana namespace", func(t *testing.T) {
   168  			job := newJob(namespace)
   169  			request := newRequest(job)
   170  			request.Job.ConsulToken = entTokenBananaNS
   171  			var response structs.JobRegisterResponse
   172  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   173  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   174  		})
   175  	})
   176  
   177  	t.Run("group consul namespace banana", func(t *testing.T) {
   178  		// Nomad OSS does not respect setting the consul namespace field on the group,
   179  		// and for backwards compatibility accepts tokens in the "default" namespace
   180  		// for groups with no namespace set. The net result is setting the group namespace
   181  		// to something like "banana" and using a token in "default" namespace will
   182  		// be accepted in Nomad OSS (assuming sufficient service write policy).
   183  		//
   184  		// Using a Consul token in the non-"default" namespace will always fail in
   185  		// Nomad OSS, again because the group namespace is ignored.
   186  		namespace := "banana"
   187  
   188  		t.Run("no token provided", func(t *testing.T) {
   189  			request := newRequest(newJob(namespace))
   190  			request.Job.ConsulToken = missingToken
   191  			var response structs.JobRegisterResponse
   192  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   193  			require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
   194  		})
   195  
   196  		t.Run("unknown token provided", func(t *testing.T) {
   197  			request := newRequest(newJob(namespace))
   198  			request.Job.ConsulToken = fakeToken
   199  			var response structs.JobRegisterResponse
   200  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   201  			require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
   202  		})
   203  
   204  		t.Run("unauthorized oss token provided", func(t *testing.T) {
   205  			request := newRequest(newJob(namespace))
   206  			request.Job.ConsulToken = ossTokenNoPolicyNoNS
   207  			var response structs.JobRegisterResponse
   208  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   209  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   210  		})
   211  
   212  		t.Run("authorized oss token provided", func(t *testing.T) {
   213  			job := newJob(namespace)
   214  			request := newRequest(job)
   215  			request.Job.ConsulToken = ossTokenNoNS
   216  			var response structs.JobRegisterResponse
   217  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   218  			require.NoError(t, err)
   219  			noTokenOnJob(t, job)
   220  		})
   221  
   222  		t.Run("unauthorized token in default namespace", func(t *testing.T) {
   223  			job := newJob(namespace)
   224  			request := newRequest(job)
   225  			request.Job.ConsulToken = entTokenNoPolicyDefaultNS
   226  			var response structs.JobRegisterResponse
   227  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   228  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   229  		})
   230  
   231  		t.Run("authorized token in default namespace", func(t *testing.T) {
   232  			job := newJob(namespace)
   233  			request := newRequest(job)
   234  			request.Job.ConsulToken = entTokenDefaultNS
   235  			var response structs.JobRegisterResponse
   236  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   237  			require.NoError(t, err)
   238  			noTokenOnJob(t, job)
   239  		})
   240  
   241  		// Consul token in custom namespace will always fail in nomad oss
   242  
   243  		t.Run("unauthorized token in banana namespace", func(t *testing.T) {
   244  			job := newJob(namespace)
   245  			request := newRequest(job)
   246  			request.Job.ConsulToken = entTokenNoPolicyBananaNS
   247  			var response structs.JobRegisterResponse
   248  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   249  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   250  		})
   251  
   252  		t.Run("authorized token in banana namespace", func(t *testing.T) {
   253  			job := newJob(namespace)
   254  			request := newRequest(job)
   255  			request.Job.ConsulToken = entTokenBananaNS
   256  			var response structs.JobRegisterResponse
   257  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   258  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   259  		})
   260  	})
   261  
   262  	t.Run("group consul namespace default", func(t *testing.T) {
   263  		// Nomad OSS ignores the group consul namespace, and setting it as default
   264  		// should effectively be the same as leaving it unset.
   265  		namespace := "default"
   266  
   267  		t.Run("no token provided", func(t *testing.T) {
   268  			request := newRequest(newJob(namespace))
   269  			request.Job.ConsulToken = missingToken
   270  			var response structs.JobRegisterResponse
   271  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   272  			require.EqualError(t, err, "job-submitter consul token denied: missing consul token")
   273  		})
   274  
   275  		t.Run("unknown token provided", func(t *testing.T) {
   276  			request := newRequest(newJob(namespace))
   277  			request.Job.ConsulToken = fakeToken
   278  			var response structs.JobRegisterResponse
   279  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   280  			require.EqualError(t, err, "job-submitter consul token denied: unable to read consul token: no such token")
   281  		})
   282  
   283  		t.Run("unauthorized oss token provided", func(t *testing.T) {
   284  			request := newRequest(newJob(namespace))
   285  			request.Job.ConsulToken = ossTokenNoPolicyNoNS
   286  			var response structs.JobRegisterResponse
   287  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   288  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   289  		})
   290  
   291  		t.Run("authorized oss token provided", func(t *testing.T) {
   292  			job := newJob(namespace)
   293  			request := newRequest(job)
   294  			request.Job.ConsulToken = ossTokenNoNS
   295  			var response structs.JobRegisterResponse
   296  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   297  			require.NoError(t, err)
   298  			noTokenOnJob(t, job)
   299  		})
   300  
   301  		t.Run("unauthorized token in default namespace", func(t *testing.T) {
   302  			job := newJob(namespace)
   303  			request := newRequest(job)
   304  			request.Job.ConsulToken = entTokenNoPolicyDefaultNS
   305  			var response structs.JobRegisterResponse
   306  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   307  			require.EqualError(t, err, `job-submitter consul token denied: insufficient Consul ACL permissions to write service "service1"`)
   308  		})
   309  
   310  		t.Run("authorized token in default namespace", func(t *testing.T) {
   311  			job := newJob(namespace)
   312  			request := newRequest(job)
   313  			request.Job.ConsulToken = entTokenDefaultNS
   314  			var response structs.JobRegisterResponse
   315  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   316  			require.NoError(t, err)
   317  			noTokenOnJob(t, job)
   318  		})
   319  
   320  		t.Run("unauthorized token in banana namespace", func(t *testing.T) {
   321  			job := newJob(namespace)
   322  			request := newRequest(job)
   323  			request.Job.ConsulToken = entTokenNoPolicyBananaNS
   324  			var response structs.JobRegisterResponse
   325  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   326  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   327  		})
   328  
   329  		t.Run("authorized token in banana namespace", func(t *testing.T) {
   330  			job := newJob(namespace)
   331  			request := newRequest(job)
   332  			request.Job.ConsulToken = entTokenBananaNS
   333  			var response structs.JobRegisterResponse
   334  			err := msgpackrpc.CallWithCodec(codec, "Job.Register", request, &response)
   335  			require.EqualError(t, err, `job-submitter consul token denied: consul ACL token requires using namespace "banana"`)
   336  		})
   337  	})
   338  }
   339  
   340  func TestJobEndpoint_Register_NodePool(t *testing.T) {
   341  	ci.Parallel(t)
   342  
   343  	s, cleanupS := TestServer(t, func(c *Config) {
   344  		c.NumSchedulers = 0
   345  	})
   346  	defer cleanupS()
   347  	codec := rpcClient(t, s)
   348  	testutil.WaitForLeader(t, s.RPC)
   349  
   350  	// Create test namespace.
   351  	ns := mock.Namespace()
   352  	nsReq := &structs.NamespaceUpsertRequest{
   353  		Namespaces:   []*structs.Namespace{ns},
   354  		WriteRequest: structs.WriteRequest{Region: "global"},
   355  	}
   356  	var nsResp structs.GenericResponse
   357  	err := msgpackrpc.CallWithCodec(codec, "Namespace.UpsertNamespaces", nsReq, &nsResp)
   358  	must.NoError(t, err)
   359  
   360  	// Create test node pool.
   361  	pool := mock.NodePool()
   362  	poolReq := &structs.NodePoolUpsertRequest{
   363  		NodePools:    []*structs.NodePool{pool},
   364  		WriteRequest: structs.WriteRequest{Region: "global"},
   365  	}
   366  	var poolResp structs.GenericResponse
   367  	err = msgpackrpc.CallWithCodec(codec, "NodePool.UpsertNodePools", poolReq, &poolResp)
   368  	must.NoError(t, err)
   369  
   370  	testCases := []struct {
   371  		name         string
   372  		namespace    string
   373  		nodePool     string
   374  		expectedPool string
   375  		expectedErr  string
   376  	}{
   377  		{
   378  			name:         "job in default namespace uses default node pool",
   379  			namespace:    structs.DefaultNamespace,
   380  			nodePool:     "",
   381  			expectedPool: structs.NodePoolDefault,
   382  		},
   383  		{
   384  			name:         "job without node pool uses default node pool",
   385  			namespace:    ns.Name,
   386  			nodePool:     "",
   387  			expectedPool: structs.NodePoolDefault,
   388  		},
   389  		{
   390  			name:         "job can set node pool",
   391  			namespace:    ns.Name,
   392  			nodePool:     pool.Name,
   393  			expectedPool: pool.Name,
   394  		},
   395  	}
   396  
   397  	for _, tc := range testCases {
   398  		t.Run(tc.name, func(t *testing.T) {
   399  			job := mock.Job()
   400  			job.Namespace = tc.namespace
   401  			job.NodePool = tc.nodePool
   402  
   403  			req := &structs.JobRegisterRequest{
   404  				Job: job,
   405  				WriteRequest: structs.WriteRequest{
   406  					Region:    "global",
   407  					Namespace: job.Namespace,
   408  				},
   409  			}
   410  			var resp structs.JobRegisterResponse
   411  			err = msgpackrpc.CallWithCodec(codec, "Job.Register", req, &resp)
   412  
   413  			if tc.expectedErr != "" {
   414  				must.ErrorContains(t, err, tc.expectedErr)
   415  			} else {
   416  				must.NoError(t, err)
   417  
   418  				got, err := s.State().JobByID(nil, job.Namespace, job.ID)
   419  				must.NoError(t, err)
   420  				must.Eq(t, tc.expectedPool, got.NodePool)
   421  			}
   422  		})
   423  	}
   424  }