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 }