diff --git a/command/agent/alloc_endpoint.go b/command/agent/alloc_endpoint.go index 0116d8cf3..e9127a846 100644 --- a/command/agent/alloc_endpoint.go +++ b/command/agent/alloc_endpoint.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/golang/snappy" + "github.com/hashicorp/nomad/acl" "github.com/hashicorp/nomad/nomad/structs" ) @@ -118,6 +119,18 @@ func (s *HTTPServer) ClientGCRequest(resp http.ResponseWriter, req *http.Request } func (s *HTTPServer) allocGC(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + var secret string + s.parseToken(req, &secret) + + var namespace string + parseNamespace(req, &namespace) + + // Check namespace submit-job permissions + if aclObj, err := s.agent.Client().ResolveToken(secret); err != nil { + return nil, err + } else if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilitySubmitJob) { + return nil, structs.ErrPermissionDenied + } return nil, s.agent.Client().CollectAllocation(allocID) } @@ -133,6 +146,19 @@ func (s *HTTPServer) allocSnapshot(allocID string, resp http.ResponseWriter, req } func (s *HTTPServer) allocStats(allocID string, resp http.ResponseWriter, req *http.Request) (interface{}, error) { + var secret string + s.parseToken(req, &secret) + + var namespace string + parseNamespace(req, &namespace) + + // Check namespace read-job permissions + if aclObj, err := s.agent.Client().ResolveToken(secret); err != nil { + return nil, err + } else if aclObj != nil && !aclObj.AllowNsOp(namespace, acl.NamespaceCapabilityReadJob) { + return nil, structs.ErrPermissionDenied + } + clientStats := s.agent.client.StatsReporter() aStats, err := clientStats.GetAllocStats(allocID) if err != nil { diff --git a/command/agent/alloc_endpoint_test.go b/command/agent/alloc_endpoint_test.go index 4a223a471..ab552e1ac 100644 --- a/command/agent/alloc_endpoint_test.go +++ b/command/agent/alloc_endpoint_test.go @@ -243,6 +243,61 @@ func TestHTTP_AllocStats(t *testing.T) { }) } +func TestHTTP_AllocStats_ACL(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + httpACLTest(t, nil, func(s *TestAgent) { + state := s.Agent.server.State() + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/client/allocation/123/stats", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Try request without a token and expect failure + { + respW := httptest.NewRecorder() + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try request with an invalid token and expect failure + { + respW := httptest.NewRecorder() + token := mock.CreatePolicyAndToken(t, state, 1005, "invalid", mock.NodePolicy(acl.PolicyWrite)) + setToken(req, token) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try request with a valid token + // Still returns an error because the alloc does not exist + { + respW := httptest.NewRecorder() + policy := mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}) + token := mock.CreatePolicyAndToken(t, state, 1007, "valid", policy) + setToken(req, token) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Contains(err.Error(), "unknown allocation ID") + } + + // Try request with a management token + // Still returns an error because the alloc does not exist + { + respW := httptest.NewRecorder() + setToken(req, s.RootToken) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Contains(err.Error(), "unknown allocation ID") + } + }) +} + func TestHTTP_AllocSnapshot(t *testing.T) { t.Parallel() httpTest(t, nil, func(s *TestAgent) { @@ -279,6 +334,61 @@ func TestHTTP_AllocGC(t *testing.T) { }) } +func TestHTTP_AllocGC_ACL(t *testing.T) { + t.Parallel() + assert := assert.New(t) + + httpACLTest(t, nil, func(s *TestAgent) { + state := s.Agent.server.State() + + // Make the HTTP request + req, err := http.NewRequest("GET", "/v1/client/allocation/123/gc", nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Try request without a token and expect failure + { + respW := httptest.NewRecorder() + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try request with an invalid token and expect failure + { + respW := httptest.NewRecorder() + token := mock.CreatePolicyAndToken(t, state, 1005, "invalid", mock.NodePolicy(acl.PolicyWrite)) + setToken(req, token) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try request with a valid token + // Still returns an error because the alloc does not exist + { + respW := httptest.NewRecorder() + policy := mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilitySubmitJob}) + token := mock.CreatePolicyAndToken(t, state, 1007, "valid", policy) + setToken(req, token) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Contains(err.Error(), "not present") + } + + // Try request with a management token + // Still returns an error because the alloc does not exist + { + respW := httptest.NewRecorder() + setToken(req, s.RootToken) + _, err := s.Server.ClientAllocRequest(respW, req) + assert.NotNil(err) + assert.Contains(err.Error(), "not present") + } + }) +} + func TestHTTP_AllocAllGC(t *testing.T) { t.Parallel() httpTest(t, nil, func(s *TestAgent) { diff --git a/website/source/api/client.html.md b/website/source/api/client.html.md index 9015e36c5..25db68387 100644 --- a/website/source/api/client.html.md +++ b/website/source/api/client.html.md @@ -147,9 +147,9 @@ The table below shows this endpoint's support for [blocking queries](/api/index.html#blocking-queries) and [required ACLs](/api/index.html#acls). -| Blocking Queries | ACL Required | -| ---------------- | ------------ | -| `NO` | `none` | +| Blocking Queries | ACL Required | +| ---------------- | -------------------- | +| `NO` | `namespace:read-job` | ### Parameters