diff --git a/agent/grpc-external/services/resource/testing/testing.go b/agent/grpc-external/services/resource/testing/testing.go index 5bcbc148e7bb..b315f9405faa 100644 --- a/agent/grpc-external/services/resource/testing/testing.go +++ b/agent/grpc-external/services/resource/testing/testing.go @@ -8,15 +8,42 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl/resolver" svc "github.com/hashicorp/consul/agent/grpc-external/services/resource" internal "github.com/hashicorp/consul/agent/grpc-internal" + "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/storage/inmem" "github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/sdk/testutil" + "github.com/hashicorp/go-uuid" ) +func randomACLIdentity(t *testing.T) structs.ACLIdentity { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + + return &structs.ACLToken{AccessorID: id} +} + +func AuthorizerFrom(t *testing.T, policyStrs ...string) resolver.Result { + policies := []*acl.Policy{} + for _, policyStr := range policyStrs { + policy, err := acl.NewPolicyFromSource(policyStr, nil, nil) + require.NoError(t, err) + policies = append(policies, policy) + } + + authz, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), policies, nil) + require.NoError(t, err) + + return resolver.Result{ + Authorizer: authz, + ACLIdentity: randomACLIdentity(t), + } +} + // RunResourceService runs a Resource Service for the duration of the test and // returns a client to interact with it. ACLs will be disabled. func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient { @@ -57,3 +84,42 @@ func RunResourceService(t *testing.T, registerFns ...func(resource.Registry)) pb return pbresource.NewResourceServiceClient(conn) } + +func RunResourceServiceWithACL(t *testing.T, aclResolver svc.ACLResolver, registerFns ...func(resource.Registry)) pbresource.ResourceServiceClient { + t.Helper() + + backend, err := inmem.NewBackend() + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + go backend.Run(ctx) + + registry := resource.NewRegistry() + for _, fn := range registerFns { + fn(registry) + } + + server := grpc.NewServer() + + svc.NewServer(svc.Config{ + Backend: backend, + Registry: registry, + Logger: testutil.Logger(t), + ACLResolver: aclResolver, + }).Register(server) + + pipe := internal.NewPipeListener() + go server.Serve(pipe) + t.Cleanup(server.Stop) + + conn, err := grpc.Dial("", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithContextDialer(pipe.DialContext), + grpc.WithBlock(), + ) + require.NoError(t, err) + t.Cleanup(func() { _ = conn.Close() }) + + return pbresource.NewResourceServiceClient(conn) +} diff --git a/internal/resource/http/http_test.go b/internal/resource/http/http_test.go index 168c1d5d2a0e..ae152135ca4c 100644 --- a/internal/resource/http/http_test.go +++ b/internal/resource/http/http_test.go @@ -8,9 +8,13 @@ import ( "testing" "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/acl" + resourceSvc "github.com/hashicorp/consul/agent/grpc-external/services/resource" svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" + "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource/demo" "github.com/hashicorp/consul/proto-public/pbresource" @@ -18,9 +22,20 @@ import ( "github.com/hashicorp/consul/sdk/testutil" ) -func TestResourceHandler(t *testing.T) { - client := svctest.RunResourceService(t, demo.RegisterTypes) +const testACLToken = acl.AnonymousTokenID + +func parseToken(req *http.Request, token *string) { + *token = req.Header.Get("x-Consul-Token") +} +func TestResourceHandler_InputValidation(t *testing.T) { + type testCase struct { + description string + request *http.Request + response *httptest.ResponseRecorder + expectedResponseCode int + } + client := svctest.RunResourceService(t, demo.RegisterTypes) resourceHandler := resourceHandler{ resource.Registration{ Type: demo.TypeV2Artist, @@ -31,43 +46,66 @@ func TestResourceHandler(t *testing.T) { hclog.NewNullLogger(), } - t.Run("should return bad request due to missing resource name", func(t *testing.T) { - rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` - { - "metadata": { - "foo": "bar" - }, - "data": { - "name": "Keith Urban", - "genre": "GENRE_COUNTRY" + testCases := []testCase{ + { + description: "missing resource name", + request: httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` + { + "metadata": { + "foo": "bar" + }, + "data": { + "name": "Keith Urban", + "genre": "GENRE_COUNTRY" + } } - } - `)) + `)), + response: httptest.NewRecorder(), + expectedResponseCode: http.StatusBadRequest, + }, + { + description: "wrong schema", + request: httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` + { + "metadata": { + "foo": "bar" + }, + "tada": { + "name": "Keith Urban", + "genre": "GENRE_COUNTRY" + } + } + `)), + response: httptest.NewRecorder(), + expectedResponseCode: http.StatusBadRequest, + }, + } - resourceHandler.ServeHTTP(rsp, req) + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + resourceHandler.ServeHTTP(tc.response, tc.request) - require.Equal(t, http.StatusBadRequest, rsp.Result().StatusCode) - }) + require.Equal(t, tc.expectedResponseCode, tc.response.Result().StatusCode) + }) + } +} - t.Run("should return bad request due to wrong schema", func(t *testing.T) { - rsp := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/?partition=default&peer_name=local&namespace=default", strings.NewReader(` - { - "metadata": { - "foo": "bar" - }, - "tada": { - "name": "Keith Urban", - "genre": "GENRE_COUNTRY" - } - } - `)) +func TestResourceWriteHandler(t *testing.T) { + aclResolver := &resourceSvc.MockACLResolver{} + aclResolver.On("ResolveTokenAndDefaultMeta", testACLToken, mock.Anything, mock.Anything). + Return(svctest.AuthorizerFrom(t, demo.ArtistV2WritePolicy), nil) - resourceHandler.ServeHTTP(rsp, req) + client := svctest.RunResourceServiceWithACL(t, aclResolver, demo.RegisterTypes) - require.Equal(t, http.StatusBadRequest, rsp.Result().StatusCode) - }) + resourceHandler := resourceHandler{ + resource.Registration{ + Type: demo.TypeV2Artist, + Proto: &pbdemov2.Artist{}, + }, + client, + parseToken, + hclog.NewNullLogger(), + } t.Run("should write to the resource backend", func(t *testing.T) { rsp := httptest.NewRecorder() @@ -83,6 +121,8 @@ func TestResourceHandler(t *testing.T) { } `)) + req.Header.Add("x-consul-token", testACLToken) + resourceHandler.ServeHTTP(rsp, req) require.Equal(t, http.StatusOK, rsp.Result().StatusCode)