github.com/hernad/nomad@v1.6.112/nomad/variables_endpoint_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package nomad 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "math/rand" 10 "strings" 11 "testing" 12 "time" 13 14 msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" 15 "github.com/shoenig/test/must" 16 17 "github.com/hernad/nomad/acl" 18 "github.com/hernad/nomad/ci" 19 "github.com/hernad/nomad/helper/uuid" 20 "github.com/hernad/nomad/nomad/mock" 21 "github.com/hernad/nomad/nomad/structs" 22 "github.com/hernad/nomad/testutil" 23 ) 24 25 func TestVariablesEndpoint_auth(t *testing.T) { 26 27 ci.Parallel(t) 28 srv, _, shutdown := TestACLServer(t, func(c *Config) { 29 c.NumSchedulers = 0 // Prevent automatic dequeue 30 }) 31 defer shutdown() 32 testutil.WaitForLeader(t, srv.RPC) 33 34 const ns = "nondefault-namespace" 35 36 alloc1 := mock.Alloc() 37 alloc1.ClientStatus = structs.AllocClientStatusFailed 38 alloc1.Job.Namespace = ns 39 alloc1.Namespace = ns 40 jobID := alloc1.JobID 41 42 // create an alloc that will have no access to variables we create 43 alloc2 := mock.Alloc() 44 alloc2.Job.TaskGroups[0].Name = "other-no-permissions" 45 alloc2.TaskGroup = "other-no-permissions" 46 alloc2.ClientStatus = structs.AllocClientStatusRunning 47 alloc2.Job.Namespace = ns 48 alloc2.Namespace = ns 49 50 alloc3 := mock.Alloc() 51 alloc3.ClientStatus = structs.AllocClientStatusRunning 52 alloc3.Job.Namespace = ns 53 alloc3.Namespace = ns 54 parentID := uuid.Short() 55 alloc3.Job.ParentID = parentID 56 57 alloc4 := mock.Alloc() 58 alloc4.ClientStatus = structs.AllocClientStatusRunning 59 alloc4.Job.Namespace = ns 60 alloc4.Namespace = ns 61 62 store := srv.fsm.State() 63 must.NoError(t, store.UpsertNamespaces(1000, []*structs.Namespace{{Name: ns}})) 64 must.NoError(t, store.UpsertAllocs( 65 structs.MsgTypeTestSetup, 1001, []*structs.Allocation{alloc1, alloc2, alloc3, alloc4})) 66 67 claims1 := alloc1.ToTaskIdentityClaims(nil, "web") 68 idToken, _, err := srv.encrypter.SignClaims(claims1) 69 must.NoError(t, err) 70 71 claims2 := alloc2.ToTaskIdentityClaims(nil, "web") 72 noPermissionsToken, _, err := srv.encrypter.SignClaims(claims2) 73 must.NoError(t, err) 74 75 claims3 := alloc3.ToTaskIdentityClaims(alloc3.Job, "web") 76 idDispatchToken, _, err := srv.encrypter.SignClaims(claims3) 77 must.NoError(t, err) 78 79 // corrupt the signature of the token 80 idTokenParts := strings.Split(idToken, ".") 81 must.Len(t, 3, idTokenParts) 82 sig := []string(strings.Split(idTokenParts[2], "")) 83 rand.Shuffle(len(sig), func(i, j int) { 84 sig[i], sig[j] = sig[j], sig[i] 85 }) 86 idTokenParts[2] = strings.Join(sig, "") 87 invalidIDToken := strings.Join(idTokenParts, ".") 88 89 claims4 := alloc4.ToTaskIdentityClaims(alloc4.Job, "web") 90 wiOnlyToken, _, err := srv.encrypter.SignClaims(claims4) 91 must.NoError(t, err) 92 93 policy := mock.ACLPolicy() 94 policy.Rules = fmt.Sprintf(`namespace "nondefault-namespace" { 95 variables { 96 path "nomad/jobs/*" { capabilities = ["list"] } 97 path "nomad/jobs/%s/web" { capabilities = ["deny"] } 98 path "nomad/jobs/%s" { capabilities = ["write"] } 99 path "other/path" { capabilities = ["read"] } 100 }}`, jobID, jobID) 101 policy.JobACL = &structs.JobACL{ 102 Namespace: ns, 103 JobID: jobID, 104 Group: alloc1.TaskGroup, 105 } 106 policy.SetHash() 107 err = store.UpsertACLPolicies(structs.MsgTypeTestSetup, 1100, []*structs.ACLPolicy{policy}) 108 must.NoError(t, err) 109 110 aclToken := mock.ACLToken() 111 aclToken.Policies = []string{policy.Name} 112 err = store.UpsertACLTokens(structs.MsgTypeTestSetup, 1150, []*structs.ACLToken{aclToken}) 113 must.NoError(t, err) 114 115 variablesRPC := NewVariablesEndpoint(srv, nil, srv.encrypter) 116 117 testFn := func(args *structs.QueryOptions, cap, path string) error { 118 err := srv.Authenticate(nil, args) 119 if err != nil { 120 return structs.ErrPermissionDenied 121 } 122 _, _, err = variablesRPC.handleMixedAuthEndpoint( 123 *args, cap, path) 124 return err 125 } 126 127 t.Run("terminal alloc should be denied", func(t *testing.T) { 128 err := testFn( 129 &structs.QueryOptions{AuthToken: idToken, Namespace: ns}, acl.PolicyList, 130 fmt.Sprintf("nomad/jobs/%s/web/web", jobID)) 131 must.EqError(t, err, structs.ErrPermissionDenied.Error()) 132 }) 133 134 // make alloc non-terminal 135 alloc1.ClientStatus = structs.AllocClientStatusRunning 136 must.NoError(t, store.UpsertAllocs( 137 structs.MsgTypeTestSetup, 1200, []*structs.Allocation{alloc1})) 138 139 t.Run("wrong namespace should be denied", func(t *testing.T) { 140 err := testFn(&structs.QueryOptions{ 141 AuthToken: idToken, Namespace: structs.DefaultNamespace}, acl.PolicyList, 142 fmt.Sprintf("nomad/jobs/%s/web/web", jobID)) 143 must.EqError(t, err, structs.ErrPermissionDenied.Error()) 144 }) 145 146 testCases := []struct { 147 name string 148 token string 149 cap string 150 path string 151 expectedErr error 152 }{ 153 { 154 name: "WI with policy no override can read task secret", 155 token: idToken, 156 cap: acl.PolicyRead, 157 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 158 expectedErr: nil, 159 }, 160 { 161 name: "WI with policy no override can list task secret", 162 token: idToken, 163 cap: acl.PolicyList, 164 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 165 expectedErr: nil, 166 }, 167 { 168 name: "WI with policy override denies list group secret", 169 token: idToken, 170 cap: acl.PolicyList, 171 path: fmt.Sprintf("nomad/jobs/%s/web", jobID), 172 expectedErr: structs.ErrPermissionDenied, 173 }, 174 { 175 name: "WI with policy override can write job secret", 176 token: idToken, 177 cap: acl.PolicyWrite, 178 path: fmt.Sprintf("nomad/jobs/%s", jobID), 179 expectedErr: nil, 180 }, 181 { 182 name: "WI with policy override for write-only job secret", 183 token: idToken, 184 cap: acl.PolicyRead, 185 path: fmt.Sprintf("nomad/jobs/%s", jobID), 186 expectedErr: structs.ErrPermissionDenied, 187 }, 188 { 189 name: "WI with policy no override can list namespace secret", 190 token: idToken, 191 cap: acl.PolicyList, 192 path: "nomad/jobs", 193 expectedErr: nil, 194 }, 195 196 { 197 name: "WI with policy can read other path", 198 token: idToken, 199 cap: acl.PolicyRead, 200 path: "other/path", 201 expectedErr: nil, 202 }, 203 { 204 name: "WI with policy cannot read other path not explicitly allowed", 205 token: idToken, 206 cap: acl.PolicyRead, 207 path: "other/not-allowed", 208 expectedErr: structs.ErrPermissionDenied, 209 }, 210 { 211 name: "WI with policy has no write cap for other path", 212 token: idToken, 213 cap: acl.PolicyWrite, 214 path: "other/path", 215 expectedErr: structs.ErrPermissionDenied, 216 }, 217 { 218 name: "WI with policy can read cross-job path", 219 token: idToken, 220 cap: acl.PolicyList, 221 path: "nomad/jobs/some-other", 222 expectedErr: nil, 223 }, 224 225 { 226 name: "WI for dispatch job can read parent secret", 227 token: idDispatchToken, 228 cap: acl.PolicyRead, 229 path: fmt.Sprintf("nomad/jobs/%s", parentID), 230 expectedErr: nil, 231 }, 232 233 { 234 name: "valid claim with no permissions denied by path", 235 token: noPermissionsToken, 236 cap: acl.PolicyList, 237 path: fmt.Sprintf("nomad/jobs/%s/w", jobID), 238 expectedErr: structs.ErrPermissionDenied, 239 }, 240 { 241 name: "valid claim with no permissions allowed by namespace", 242 token: noPermissionsToken, 243 cap: acl.PolicyList, 244 path: "nomad/jobs", 245 expectedErr: nil, 246 }, 247 { 248 name: "valid claim with no permissions denied by capability", 249 token: noPermissionsToken, 250 cap: acl.PolicyRead, 251 path: fmt.Sprintf("nomad/jobs/%s/w", jobID), 252 expectedErr: structs.ErrPermissionDenied, 253 }, 254 { 255 name: "missing auth token is denied", 256 cap: acl.PolicyList, 257 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 258 expectedErr: structs.ErrPermissionDenied, 259 }, 260 { 261 name: "invalid signature is denied", 262 token: invalidIDToken, 263 cap: acl.PolicyList, 264 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 265 expectedErr: structs.ErrPermissionDenied, 266 }, 267 { 268 name: "invalid claim for dispatched ID", 269 token: idDispatchToken, 270 cap: acl.PolicyList, 271 path: fmt.Sprintf("nomad/jobs/%s", alloc3.JobID), 272 expectedErr: structs.ErrPermissionDenied, 273 }, 274 { 275 name: "acl token read policy is allowed to list", 276 token: aclToken.SecretID, 277 cap: acl.PolicyList, 278 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 279 expectedErr: nil, 280 }, 281 { 282 name: "acl token read policy is not allowed to write", 283 token: aclToken.SecretID, 284 cap: acl.PolicyWrite, 285 path: fmt.Sprintf("nomad/jobs/%s/web/web", jobID), 286 expectedErr: structs.ErrPermissionDenied, 287 }, 288 289 { 290 name: "WI token can read own task", 291 token: wiOnlyToken, 292 cap: acl.PolicyRead, 293 path: fmt.Sprintf("nomad/jobs/%s/web/web", alloc4.JobID), 294 expectedErr: nil, 295 }, 296 { 297 name: "WI token can list own task", 298 token: wiOnlyToken, 299 cap: acl.PolicyList, 300 path: fmt.Sprintf("nomad/jobs/%s/web/web", alloc4.JobID), 301 expectedErr: nil, 302 }, 303 { 304 name: "WI token can read own group", 305 token: wiOnlyToken, 306 cap: acl.PolicyRead, 307 path: fmt.Sprintf("nomad/jobs/%s/web", alloc4.JobID), 308 expectedErr: nil, 309 }, 310 { 311 name: "WI token can list own group", 312 token: wiOnlyToken, 313 cap: acl.PolicyList, 314 path: fmt.Sprintf("nomad/jobs/%s/web", alloc4.JobID), 315 expectedErr: nil, 316 }, 317 318 { 319 name: "WI token cannot read another task in group", 320 token: wiOnlyToken, 321 cap: acl.PolicyRead, 322 path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID), 323 expectedErr: structs.ErrPermissionDenied, 324 }, 325 { 326 name: "WI token cannot list another task in group", 327 token: wiOnlyToken, 328 cap: acl.PolicyList, 329 path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID), 330 expectedErr: structs.ErrPermissionDenied, 331 }, 332 { 333 name: "WI token cannot read another task in group", 334 token: wiOnlyToken, 335 cap: acl.PolicyRead, 336 path: fmt.Sprintf("nomad/jobs/%s/web/other", alloc4.JobID), 337 expectedErr: structs.ErrPermissionDenied, 338 }, 339 { 340 name: "WI token cannot list a task in another group", 341 token: wiOnlyToken, 342 cap: acl.PolicyRead, 343 path: fmt.Sprintf("nomad/jobs/%s/other/web", alloc4.JobID), 344 expectedErr: structs.ErrPermissionDenied, 345 }, 346 { 347 name: "WI token cannot read a task in another group", 348 token: wiOnlyToken, 349 cap: acl.PolicyRead, 350 path: fmt.Sprintf("nomad/jobs/%s/other/web", alloc4.JobID), 351 expectedErr: structs.ErrPermissionDenied, 352 }, 353 { 354 name: "WI token cannot read a group in another job", 355 token: wiOnlyToken, 356 cap: acl.PolicyRead, 357 path: "nomad/jobs/other/web/web", 358 expectedErr: structs.ErrPermissionDenied, 359 }, 360 { 361 name: "WI token cannot list a group in another job", 362 token: wiOnlyToken, 363 cap: acl.PolicyList, 364 path: "nomad/jobs/other/web/web", 365 expectedErr: structs.ErrPermissionDenied, 366 }, 367 368 { 369 name: "WI token extra trailing slash is denied", 370 token: wiOnlyToken, 371 cap: acl.PolicyList, 372 path: fmt.Sprintf("nomad/jobs/%s/web/", alloc4.JobID), 373 expectedErr: structs.ErrPermissionDenied, 374 }, 375 { 376 name: "WI token invalid prefix is denied", 377 token: wiOnlyToken, 378 cap: acl.PolicyList, 379 path: fmt.Sprintf("nomad/jobs/%s/w", alloc4.JobID), 380 expectedErr: structs.ErrPermissionDenied, 381 }, 382 } 383 384 for _, tc := range testCases { 385 tc := tc 386 t.Run(tc.name, func(t *testing.T) { 387 err := testFn( 388 &structs.QueryOptions{AuthToken: tc.token, Namespace: ns}, 389 tc.cap, tc.path) 390 if tc.expectedErr == nil { 391 must.NoError(t, err) 392 } else { 393 must.EqError(t, err, tc.expectedErr.Error()) 394 } 395 }) 396 } 397 398 } 399 400 func TestVariablesEndpoint_Apply_ACL(t *testing.T) { 401 ci.Parallel(t) 402 srv, rootToken, shutdown := TestACLServer(t, func(c *Config) { 403 c.NumSchedulers = 0 // Prevent automatic dequeue 404 }) 405 defer shutdown() 406 testutil.WaitForLeader(t, srv.RPC) 407 codec := rpcClient(t, srv) 408 state := srv.fsm.State() 409 410 pol := mock.NamespacePolicyWithVariables( 411 structs.DefaultNamespace, "", []string{"list-jobs"}, 412 map[string][]string{ 413 "dropbox/*": {"write"}, 414 }) 415 writeToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid", pol) 416 417 sv1 := mock.Variable() 418 sv1.ModifyIndex = 0 419 var svHold *structs.VariableDecrypted 420 421 opMap := map[string]structs.VarOp{ 422 "set": structs.VarOpSet, 423 "cas": structs.VarOpCAS, 424 "delete": structs.VarOpDelete, 425 "delete-cas": structs.VarOpDeleteCAS, 426 } 427 428 for name, op := range opMap { 429 t.Run(name+"/no token", func(t *testing.T) { 430 sv1 := sv1 431 applyReq := structs.VariablesApplyRequest{ 432 Op: op, 433 Var: sv1, 434 WriteRequest: structs.WriteRequest{Region: "global"}, 435 } 436 applyResp := new(structs.VariablesApplyResponse) 437 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, applyResp) 438 must.EqError(t, err, structs.ErrPermissionDenied.Error()) 439 }) 440 } 441 442 t.Run("cas/management token/new", func(t *testing.T) { 443 applyReq := structs.VariablesApplyRequest{ 444 Op: structs.VarOpCAS, 445 Var: sv1, 446 WriteRequest: structs.WriteRequest{ 447 Region: "global", 448 AuthToken: rootToken.SecretID, 449 }, 450 } 451 applyResp := new(structs.VariablesApplyResponse) 452 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, applyResp) 453 454 must.NoError(t, err) 455 must.Eq(t, structs.VarOpResultOk, applyResp.Result) 456 must.Eq(t, sv1.Items, applyResp.Output.Items) 457 458 svHold = applyResp.Output 459 }) 460 461 t.Run("cas with current", func(t *testing.T) { 462 must.NotNil(t, svHold) 463 sv := svHold 464 sv.Items["new"] = "newVal" 465 466 applyReq := structs.VariablesApplyRequest{ 467 Op: structs.VarOpCAS, 468 Var: sv, 469 WriteRequest: structs.WriteRequest{ 470 Region: "global", 471 AuthToken: rootToken.SecretID, 472 }, 473 } 474 applyResp := new(structs.VariablesApplyResponse) 475 applyReq.AuthToken = rootToken.SecretID 476 477 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, &applyResp) 478 479 must.NoError(t, err) 480 must.Eq(t, structs.VarOpResultOk, applyResp.Result) 481 must.Eq(t, sv.Items, applyResp.Output.Items) 482 483 svHold = applyResp.Output 484 }) 485 486 t.Run("cas with stale", func(t *testing.T) { 487 must.NotNil(t, sv1) // TODO: query these directly 488 must.NotNil(t, svHold) 489 490 sv1 := sv1 491 svHold := svHold 492 493 applyReq := structs.VariablesApplyRequest{ 494 Op: structs.VarOpCAS, 495 Var: sv1, 496 WriteRequest: structs.WriteRequest{ 497 Region: "global", 498 AuthToken: rootToken.SecretID, 499 }, 500 } 501 applyResp := new(structs.VariablesApplyResponse) 502 applyReq.AuthToken = rootToken.SecretID 503 504 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, &applyResp) 505 506 must.NoError(t, err) 507 must.Eq(t, structs.VarOpResultConflict, applyResp.Result) 508 must.Eq(t, svHold.VariableMetadata, applyResp.Conflict.VariableMetadata) 509 must.Eq(t, svHold.Items, applyResp.Conflict.Items) 510 }) 511 512 sv3 := mock.Variable() 513 sv3.Path = "dropbox/a" 514 sv3.ModifyIndex = 0 515 516 t.Run("cas/write-only/read own new", func(t *testing.T) { 517 sv3 := sv3 518 applyReq := structs.VariablesApplyRequest{ 519 Op: structs.VarOpCAS, 520 Var: sv3, 521 WriteRequest: structs.WriteRequest{ 522 Region: "global", 523 AuthToken: writeToken.SecretID, 524 }, 525 } 526 applyResp := new(structs.VariablesApplyResponse) 527 528 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, &applyResp) 529 530 must.NoError(t, err) 531 must.Eq(t, structs.VarOpResultOk, applyResp.Result) 532 must.Eq(t, sv3.Items, applyResp.Output.Items) 533 svHold = applyResp.Output 534 }) 535 536 t.Run("cas/write only/conflict redacted", func(t *testing.T) { 537 must.NotNil(t, sv3) 538 must.NotNil(t, svHold) 539 sv3 := sv3 540 svHold := svHold 541 542 applyReq := structs.VariablesApplyRequest{ 543 Op: structs.VarOpCAS, 544 Var: sv3, 545 WriteRequest: structs.WriteRequest{ 546 Region: "global", 547 AuthToken: writeToken.SecretID, 548 }, 549 } 550 applyResp := new(structs.VariablesApplyResponse) 551 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, &applyResp) 552 553 must.NoError(t, err) 554 must.Eq(t, structs.VarOpResultRedacted, applyResp.Result) 555 must.Eq(t, svHold.VariableMetadata, applyResp.Conflict.VariableMetadata) 556 must.Nil(t, applyResp.Conflict.Items) 557 }) 558 559 t.Run("cas/write only/read own upsert", func(t *testing.T) { 560 must.NotNil(t, svHold) 561 sv := svHold 562 sv.Items["upsert"] = "read" 563 564 applyReq := structs.VariablesApplyRequest{ 565 Op: structs.VarOpCAS, 566 Var: sv, 567 WriteRequest: structs.WriteRequest{ 568 Region: "global", 569 AuthToken: writeToken.SecretID, 570 }, 571 } 572 applyResp := new(structs.VariablesApplyResponse) 573 err := msgpackrpc.CallWithCodec(codec, structs.VariablesApplyRPCMethod, &applyReq, &applyResp) 574 575 must.NoError(t, err) 576 must.Eq(t, structs.VarOpResultOk, applyResp.Result) 577 must.Eq(t, sv.Items, applyResp.Output.Items) 578 }) 579 } 580 581 func TestVariablesEndpoint_ListFiltering(t *testing.T) { 582 ci.Parallel(t) 583 srv, _, shutdown := TestACLServer(t, func(c *Config) { 584 c.NumSchedulers = 0 // Prevent automatic dequeue 585 }) 586 defer shutdown() 587 testutil.WaitForLeader(t, srv.RPC) 588 codec := rpcClient(t, srv) 589 590 ns := "nondefault-namespace" 591 idx := uint64(1000) 592 593 alloc := mock.Alloc() 594 alloc.Job.ID = "job1" 595 alloc.JobID = "job1" 596 alloc.TaskGroup = "group" 597 alloc.Job.TaskGroups[0].Name = "group" 598 alloc.ClientStatus = structs.AllocClientStatusRunning 599 alloc.Job.Namespace = ns 600 alloc.Namespace = ns 601 602 store := srv.fsm.State() 603 must.NoError(t, store.UpsertNamespaces(idx, []*structs.Namespace{{Name: ns}})) 604 idx++ 605 must.NoError(t, store.UpsertAllocs( 606 structs.MsgTypeTestSetup, idx, []*structs.Allocation{alloc})) 607 608 claims := alloc.ToTaskIdentityClaims(alloc.Job, "web") 609 token, _, err := srv.encrypter.SignClaims(claims) 610 must.NoError(t, err) 611 612 writeVar := func(ns, path string) { 613 idx++ 614 sv := mock.VariableEncrypted() 615 sv.Namespace = ns 616 sv.Path = path 617 resp := store.VarSet(idx, &structs.VarApplyStateRequest{ 618 Op: structs.VarOpSet, 619 Var: sv, 620 }) 621 must.NoError(t, resp.Error) 622 } 623 624 writeVar(ns, "nomad/jobs/job1/group/web") 625 writeVar(ns, "nomad/jobs/job1/group") 626 writeVar(ns, "nomad/jobs/job1") 627 628 writeVar(ns, "nomad/jobs/job1/group/other") 629 writeVar(ns, "nomad/jobs/job1/other/web") 630 writeVar(ns, "nomad/jobs/job2/group/web") 631 632 req := &structs.VariablesListRequest{ 633 QueryOptions: structs.QueryOptions{ 634 Namespace: ns, 635 Prefix: "nomad", 636 AuthToken: token, 637 Region: "global", 638 }, 639 } 640 var resp structs.VariablesListResponse 641 must.NoError(t, msgpackrpc.CallWithCodec(codec, "Variables.List", req, &resp)) 642 found := []string{} 643 for _, variable := range resp.Data { 644 found = append(found, variable.Path) 645 } 646 expect := []string{ 647 "nomad/jobs/job1", 648 "nomad/jobs/job1/group", 649 "nomad/jobs/job1/group/web", 650 } 651 must.Eq(t, expect, found) 652 653 // Associate a policy with the identity's job to deny partial access. 654 policy := &structs.ACLPolicy{ 655 Name: "policy-for-identity", 656 Rules: mock.NamespacePolicyWithVariables(ns, "read", []string{}, 657 map[string][]string{"nomad/jobs/job1/group": []string{"deny"}}), 658 JobACL: &structs.JobACL{ 659 Namespace: ns, 660 JobID: "job1", 661 }, 662 } 663 policy.SetHash() 664 must.NoError(t, store.UpsertACLPolicies(structs.MsgTypeTestSetup, 16, 665 []*structs.ACLPolicy{policy})) 666 667 must.NoError(t, msgpackrpc.CallWithCodec(codec, "Variables.List", req, &resp)) 668 found = []string{} 669 for _, variable := range resp.Data { 670 found = append(found, variable.Path) 671 } 672 expect = []string{ 673 "nomad/jobs/job1", 674 "nomad/jobs/job1/group/web", 675 } 676 must.Eq(t, expect, found) 677 678 } 679 680 func TestVariablesEndpoint_ComplexACLPolicies(t *testing.T) { 681 682 ci.Parallel(t) 683 srv, _, shutdown := TestACLServer(t, func(c *Config) { 684 c.NumSchedulers = 0 // Prevent automatic dequeue 685 }) 686 defer shutdown() 687 testutil.WaitForLeader(t, srv.RPC) 688 codec := rpcClient(t, srv) 689 690 idx := uint64(1000) 691 692 policyRules := ` 693 namespace "dev" { 694 variables { 695 path "*" { capabilities = ["list", "read"] } 696 path "system/*" { capabilities = ["deny"] } 697 path "config/system/*" { capabilities = ["deny"] } 698 } 699 } 700 701 namespace "prod" { 702 variables { 703 path "*" { 704 capabilities = ["list"] 705 } 706 } 707 } 708 709 namespace "*" {} 710 ` 711 712 store := srv.fsm.State() 713 714 must.NoError(t, store.UpsertNamespaces(1000, []*structs.Namespace{ 715 {Name: "dev"}, {Name: "prod"}, {Name: "other"}})) 716 717 idx++ 718 token := mock.CreatePolicyAndToken(t, store, idx, "developer", policyRules) 719 720 writeVar := func(ns, path string) { 721 idx++ 722 sv := mock.VariableEncrypted() 723 sv.Namespace = ns 724 sv.Path = path 725 resp := store.VarSet(idx, &structs.VarApplyStateRequest{ 726 Op: structs.VarOpSet, 727 Var: sv, 728 }) 729 must.NoError(t, resp.Error) 730 } 731 732 writeVar("dev", "system/never-list") 733 writeVar("dev", "config/system/never-list") 734 writeVar("dev", "config/can-read") 735 writeVar("dev", "project/can-read") 736 737 writeVar("prod", "system/can-list") 738 writeVar("prod", "config/system/can-list") 739 writeVar("prod", "config/can-list") 740 writeVar("prod", "project/can-list") 741 742 writeVar("other", "system/never-list") 743 writeVar("other", "config/system/never-list") 744 writeVar("other", "config/never-list") 745 writeVar("other", "project/never-list") 746 747 testListPrefix := func(ns, prefix string, expectedCount int, expectErr error) { 748 t.Run(fmt.Sprintf("ns=%s-prefix=%s", ns, prefix), func(t *testing.T) { 749 req := &structs.VariablesListRequest{ 750 QueryOptions: structs.QueryOptions{ 751 Namespace: ns, 752 Prefix: prefix, 753 AuthToken: token.SecretID, 754 Region: "global", 755 }, 756 } 757 var resp structs.VariablesListResponse 758 759 if expectErr != nil { 760 must.EqError(t, 761 msgpackrpc.CallWithCodec(codec, "Variables.List", req, &resp), 762 expectErr.Error()) 763 return 764 } 765 must.NoError(t, msgpackrpc.CallWithCodec(codec, "Variables.List", req, &resp)) 766 767 found := "found:\n" 768 for _, sv := range resp.Data { 769 found += fmt.Sprintf(" ns=%s path=%s\n", sv.Namespace, sv.Path) 770 } 771 must.Len(t, expectedCount, resp.Data, must.Sprintf("%s", found)) 772 }) 773 } 774 775 testListPrefix("dev", "system", 0, nil) 776 testListPrefix("dev", "config/system", 0, nil) 777 testListPrefix("dev", "config", 1, nil) 778 testListPrefix("dev", "project", 1, nil) 779 testListPrefix("dev", "", 2, nil) 780 781 testListPrefix("prod", "system", 1, nil) 782 testListPrefix("prod", "config/system", 1, nil) 783 testListPrefix("prod", "config", 2, nil) 784 testListPrefix("prod", "project", 1, nil) 785 testListPrefix("prod", "", 4, nil) 786 787 // list gives empty but no error! 788 testListPrefix("other", "system", 0, nil) 789 testListPrefix("other", "config/system", 0, nil) 790 testListPrefix("other", "config", 0, nil) 791 testListPrefix("other", "project", 0, nil) 792 testListPrefix("other", "", 0, nil) 793 794 testListPrefix("*", "system", 1, nil) 795 testListPrefix("*", "config/system", 1, nil) 796 testListPrefix("*", "config", 3, nil) 797 testListPrefix("*", "project", 2, nil) 798 testListPrefix("*", "", 6, nil) 799 800 } 801 802 func TestVariablesEndpoint_GetVariable_Blocking(t *testing.T) { 803 ci.Parallel(t) 804 805 s1, cleanupS1 := TestServer(t, nil) 806 defer cleanupS1() 807 state := s1.fsm.State() 808 codec := rpcClient(t, s1) 809 testutil.WaitForLeader(t, s1.RPC) 810 811 // First create an unrelated variable. 812 delay := 100 * time.Millisecond 813 time.AfterFunc(delay, func() { 814 writeVar(t, s1, 100, "default", "aaa") 815 }) 816 817 // Upsert the variable we are watching later 818 delay = 200 * time.Millisecond 819 time.AfterFunc(delay, func() { 820 writeVar(t, s1, 200, "default", "bbb") 821 }) 822 823 // Lookup the variable 824 req := &structs.VariablesReadRequest{ 825 Path: "bbb", 826 QueryOptions: structs.QueryOptions{ 827 Region: "global", 828 MinQueryIndex: 150, 829 MaxQueryTime: 500 * time.Millisecond, 830 }, 831 } 832 var resp structs.VariablesReadResponse 833 start := time.Now() 834 if err := msgpackrpc.CallWithCodec(codec, "Variables.Read", req, &resp); err != nil { 835 t.Fatalf("err: %v", err) 836 } 837 elapsed := time.Since(start) 838 839 if elapsed < delay { 840 t.Fatalf("should block (returned in %s) %#v", elapsed, resp) 841 } 842 if elapsed > req.MaxQueryTime { 843 t.Fatalf("blocking query timed out %#v", resp) 844 } 845 if resp.Index != 200 { 846 t.Fatalf("Bad index: %d %d", resp.Index, 200) 847 } 848 if resp.Data == nil || resp.Data.Path != "bbb" { 849 t.Fatalf("bad: %#v", resp.Data) 850 } 851 852 // Variable update triggers watches 853 delay = 100 * time.Millisecond 854 855 time.AfterFunc(delay, func() { 856 writeVar(t, s1, 300, "default", "bbb") 857 }) 858 859 req.QueryOptions.MinQueryIndex = 250 860 var resp2 structs.VariablesReadResponse 861 start = time.Now() 862 if err := msgpackrpc.CallWithCodec(codec, "Variables.Read", req, &resp2); err != nil { 863 t.Fatalf("err: %v", err) 864 } 865 elapsed = time.Since(start) 866 867 if elapsed < delay { 868 t.Fatalf("should block (returned in %s) %#v", elapsed, resp2) 869 } 870 if elapsed > req.MaxQueryTime { 871 t.Fatal("blocking query timed out") 872 } 873 if resp2.Index != 300 { 874 t.Fatalf("Bad index: %d %d", resp2.Index, 300) 875 } 876 if resp2.Data == nil || resp2.Data.Path != "bbb" { 877 t.Fatalf("bad: %#v", resp2.Data) 878 } 879 880 // Variable delete triggers watches 881 delay = 100 * time.Millisecond 882 time.AfterFunc(delay, func() { 883 sv := mock.VariableEncrypted() 884 sv.Path = "bbb" 885 if resp := state.VarDelete(400, &structs.VarApplyStateRequest{Op: structs.VarOpDelete, Var: sv}); !resp.IsOk() { 886 t.Fatalf("err: %v", resp.Error) 887 } 888 }) 889 890 req.QueryOptions.MinQueryIndex = 350 891 var resp3 structs.VariablesReadResponse 892 start = time.Now() 893 if err := msgpackrpc.CallWithCodec(codec, "Variables.Read", req, &resp3); err != nil { 894 t.Fatalf("err: %v", err) 895 } 896 elapsed = time.Since(start) 897 898 if elapsed < delay { 899 t.Fatalf("should block (returned in %s) %#v", elapsed, resp) 900 } 901 if elapsed > req.MaxQueryTime { 902 t.Fatal("blocking query timed out") 903 } 904 if resp3.Index != 400 { 905 t.Fatalf("Bad index: %d %d", resp3.Index, 400) 906 } 907 if resp3.Data != nil { 908 t.Fatalf("bad: %#v", resp3.Data) 909 } 910 } 911 912 func writeVar(t *testing.T, s *Server, idx uint64, ns, path string) { 913 store := s.fsm.State() 914 sv := mock.Variable() 915 sv.Namespace = ns 916 sv.Path = path 917 bPlain, err := json.Marshal(sv.Items) 918 must.NoError(t, err) 919 bEnc, kID, err := s.encrypter.Encrypt(bPlain) 920 must.NoError(t, err) 921 sve := &structs.VariableEncrypted{ 922 VariableMetadata: sv.VariableMetadata, 923 VariableData: structs.VariableData{ 924 Data: bEnc, 925 KeyID: kID, 926 }, 927 } 928 resp := store.VarSet(idx, &structs.VarApplyStateRequest{ 929 Op: structs.VarOpSet, 930 Var: sve, 931 }) 932 must.NoError(t, resp.Error) 933 }