diff --git a/nomad/job_endpoint_test.go b/nomad/job_endpoint_test.go index 409d20204..62d0e4b75 100644 --- a/nomad/job_endpoint_test.go +++ b/nomad/job_endpoint_test.go @@ -5540,9 +5540,8 @@ func TestJobEndpoint_GetScaleStatus_ACL(t *testing.T) { invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid", mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs})) get.AuthToken = invalidToken.SecretID - var invalidResp structs.JobScaleStatusResponse require.NotNil(err) - err = msgpackrpc.CallWithCodec(codec, "Job.ScaleStatus", get, &invalidResp) + err = msgpackrpc.CallWithCodec(codec, "Job.ScaleStatus", get, &resp) require.Contains(err.Error(), "Permission denied") type testCase struct { diff --git a/nomad/scaling_endpoint.go b/nomad/scaling_endpoint.go index 44be49895..89e1aa0a6 100644 --- a/nomad/scaling_endpoint.go +++ b/nomad/scaling_endpoint.go @@ -7,6 +7,7 @@ import ( log "github.com/hashicorp/go-hclog" memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/nomad/state" "github.com/hashicorp/nomad/nomad/structs" ) @@ -26,31 +27,17 @@ func (a *Scaling) ListPolicies(args *structs.ScalingPolicyListRequest, } defer metrics.MeasureSince([]string{"nomad", "scaling", "list_policies"}, time.Now()) - // Check management level permissions - // acl, err := a.srv.ResolveToken(args.AuthToken) - // if err != nil { - // return err - // } else if acl == nil { - // return structs.ErrPermissionDenied - // } - - // If it is not a management token determine the policies that may be listed - // mgt := acl.IsManagement() - // var policies map[string]struct{} - // if !mgt { - // token, err := a.requestACLToken(args.AuthToken) - // if err != nil { - // return err - // } - // if token == nil { - // return structs.ErrTokenNotFound - // } - // - // policies = make(map[string]struct{}, len(token.Policies)) - // for _, p := range token.Policies { - // policies[p] = struct{}{} - // } - // } + // Check for list-job permissions + if aclObj, err := a.srv.ResolveToken(args.AuthToken); err != nil { + return err + } else if aclObj != nil { + hasListScalingPolicies := aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityListScalingPolicies) + hasListAndReadJobs := aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityListJobs) && + aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) + if !(hasListScalingPolicies || hasListAndReadJobs) { + return structs.ErrPermissionDenied + } + } // Setup the blocking query opts := blockingOptions{ @@ -103,31 +90,17 @@ func (a *Scaling) GetPolicy(args *structs.ScalingPolicySpecificRequest, } defer metrics.MeasureSince([]string{"nomad", "scaling", "get_policy"}, time.Now()) - // Check management level permissions - // acl, err := a.srv.ResolveToken(args.AuthToken) - // if err != nil { - // return err - // } else if acl == nil { - // return structs.ErrPermissionDenied - // } - - // If it is not a management token determine the policies that may be listed - // mgt := acl.IsManagement() - // var policies map[string]struct{} - // if !mgt { - // token, err := a.requestACLToken(args.AuthToken) - // if err != nil { - // return err - // } - // if token == nil { - // return structs.ErrTokenNotFound - // } - // - // policies = make(map[string]struct{}, len(token.Policies)) - // for _, p := range token.Policies { - // policies[p] = struct{}{} - // } - // } + // Check for list-job permissions + if aclObj, err := a.srv.ResolveToken(args.AuthToken); err != nil { + return err + } else if aclObj != nil { + hasReadScalingPolicy := aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadScalingPolicy) + hasListAndReadJobs := aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityListJobs) && + aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) + if !(hasReadScalingPolicy || hasListAndReadJobs) { + return structs.ErrPermissionDenied + } + } // Setup the blocking query opts := blockingOptions{ diff --git a/nomad/scaling_endpoint_test.go b/nomad/scaling_endpoint_test.go index 2a3bf36f8..9fd00de4a 100644 --- a/nomad/scaling_endpoint_test.go +++ b/nomad/scaling_endpoint_test.go @@ -1,13 +1,12 @@ package nomad import ( + msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" + "github.com/stretchr/testify/require" "testing" "time" - msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - + "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" @@ -16,7 +15,6 @@ import ( func TestScalingEndpoint_GetPolicy(t *testing.T) { t.Parallel() - require := require.New(t) s1, cleanupS1 := TestServer(t, nil) @@ -24,7 +22,6 @@ func TestScalingEndpoint_GetPolicy(t *testing.T) { codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) - // Create the register request p1 := mock.ScalingPolicy() p2 := mock.ScalingPolicy() s1.fsm.State().UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2}) @@ -39,7 +36,7 @@ func TestScalingEndpoint_GetPolicy(t *testing.T) { var resp structs.SingleScalingPolicyResponse err := msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp) require.NoError(err) - require.Equal(uint64(1000), resp.Index) + require.EqualValues(1000, resp.Index) require.Equal(*p1, *resp.Policy) // Lookup non-existing policy @@ -47,25 +44,94 @@ func TestScalingEndpoint_GetPolicy(t *testing.T) { resp = structs.SingleScalingPolicyResponse{} err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp) require.NoError(err) - require.Equal(uint64(1000), resp.Index) + require.EqualValues(1000, resp.Index) require.Nil(resp.Policy) } -func TestScalingEndpoint_ListPolicies(t *testing.T) { - assert := assert.New(t) +func TestScalingEndpoint_GetPolicy_ACL(t *testing.T) { t.Parallel() + require := require.New(t) + + s1, root, cleanupS1 := TestACLServer(t, nil) + defer cleanupS1() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + state := s1.fsm.State() + + p1 := mock.ScalingPolicy() + p2 := mock.ScalingPolicy() + state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2}) + + get := &structs.ScalingPolicySpecificRequest{ + ID: p1.ID, + QueryOptions: structs.QueryOptions{ + Region: "global", + }, + } + + // lookup without token should fail + var resp structs.SingleScalingPolicyResponse + err := msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp) + require.Error(err) + require.Contains(err.Error(), "Permission denied") + + // Expect failure for request with an invalid token + invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid", + mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies})) + get.AuthToken = invalidToken.SecretID + err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp) + require.Error(err) + require.Contains(err.Error(), "Permission denied") + type testCase struct { + authToken string + name string + } + cases := []testCase{ + { + name: "mgmt token should succeed", + authToken: root.SecretID, + }, + { + name: "read disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read", + mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)).SecretID, + }, + { + name: "write disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-write", + mock.NamespacePolicy(structs.DefaultNamespace, "write", nil)).SecretID, + }, + { + name: "autoscaler disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-autoscaler", + mock.NamespacePolicy(structs.DefaultNamespace, "autoscaler", nil)).SecretID, + }, + { + name: "list-jobs+read-job capability should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read-job-scaling", + mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs, acl.NamespaceCapabilityReadJob})).SecretID, + }, + } + + for _, tc := range cases { + get.AuthToken = tc.authToken + err = msgpackrpc.CallWithCodec(codec, "Scaling.GetPolicy", get, &resp) + require.NoError(err, tc.name) + require.EqualValues(1000, resp.Index) + require.NotNil(resp.Policy) + } + +} + +func TestScalingEndpoint_ListPolicies(t *testing.T) { + t.Parallel() + require := require.New(t) s1, cleanupS1 := TestServer(t, nil) defer cleanupS1() codec := rpcClient(t, s1) testutil.WaitForLeader(t, s1.RPC) - // Create the register request - p1 := mock.ScalingPolicy() - p2 := mock.ScalingPolicy() - - s1.fsm.State().UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2}) - // Lookup the policies get := &structs.ScalingPolicyListRequest{ QueryOptions: structs.QueryOptions{ @@ -73,16 +139,100 @@ func TestScalingEndpoint_ListPolicies(t *testing.T) { }, } var resp structs.ACLPolicyListResponse - if err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp); err != nil { - t.Fatalf("err: %v", err) + err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp) + require.NoError(err) + require.Empty(resp.Policies) + + p1 := mock.ScalingPolicy() + p2 := mock.ScalingPolicy() + s1.fsm.State().UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2}) + + err = msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp) + require.NoError(err) + require.EqualValues(1000, resp.Index) + require.Len(resp.Policies, 2) +} + +func TestScalingEndpoint_ListPolicies_ACL(t *testing.T) { + t.Parallel() + require := require.New(t) + + s1, root, cleanupS1 := TestACLServer(t, nil) + defer cleanupS1() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + state := s1.fsm.State() + + p1 := mock.ScalingPolicy() + p2 := mock.ScalingPolicy() + state.UpsertScalingPolicies(1000, []*structs.ScalingPolicy{p1, p2}) + + get := &structs.ScalingPolicyListRequest{ + QueryOptions: structs.QueryOptions{ + Region: "global", + }, + } + + // lookup without token should fail + var resp structs.ACLPolicyListResponse + err := msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp) + require.Error(err) + require.Contains(err.Error(), "Permission denied") + + // Expect failure for request with an invalid token + invalidToken := mock.CreatePolicyAndToken(t, state, 1003, "test-invalid", + mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies})) + get.AuthToken = invalidToken.SecretID + require.Error(err) + require.Contains(err.Error(), "Permission denied") + + type testCase struct { + authToken string + name string + } + cases := []testCase{ + { + name: "mgmt token should succeed", + authToken: root.SecretID, + }, + { + name: "read disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read", + mock.NamespacePolicy(structs.DefaultNamespace, "read", nil)).SecretID, + }, + { + name: "write disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-write", + mock.NamespacePolicy(structs.DefaultNamespace, "write", nil)).SecretID, + }, + { + name: "autoscaler disposition should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-autoscaler", + mock.NamespacePolicy(structs.DefaultNamespace, "autoscaler", nil)).SecretID, + }, + { + name: "list-scaling-policies capability should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-list-scaling-policies", + mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListScalingPolicies})).SecretID, + }, + { + name: "list-jobs+read-job capability should succeed", + authToken: mock.CreatePolicyAndToken(t, state, 1005, "test-valid-read-job-scaling", + mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityListJobs, acl.NamespaceCapabilityReadJob})).SecretID, + }, + } + + for _, tc := range cases { + get.AuthToken = tc.authToken + err = msgpackrpc.CallWithCodec(codec, "Scaling.ListPolicies", get, &resp) + require.NoError(err, tc.name) + require.EqualValues(1000, resp.Index) + require.Len(resp.Policies, 2) } - assert.EqualValues(1000, resp.Index) - assert.Len(resp.Policies, 2) } func TestScalingEndpoint_ListPolicies_Blocking(t *testing.T) { t.Parallel() - require := require.New(t) s1, cleanupS1 := TestServer(t, nil) @@ -120,7 +270,7 @@ func TestScalingEndpoint_ListPolicies_Blocking(t *testing.T) { require.NoError(err) require.True(time.Since(start) > 200*time.Millisecond, "should block: %#v", resp) - require.Equal(uint64(200), resp.Index, "bad index") + require.EqualValues(200, resp.Index, "bad index") require.Len(resp.Policies, 2) require.ElementsMatch([]string{p1.ID, p2.ID}, []string{resp.Policies[0].ID, resp.Policies[1].ID}) }