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  }