diff --git a/agent/consul/health_endpoint.go b/agent/consul/health_endpoint.go index 70cc2e37dd..214de777d1 100644 --- a/agent/consul/health_endpoint.go +++ b/agent/consul/health_endpoint.go @@ -130,6 +130,21 @@ func (h *Health) ServiceNodes(args *structs.ServiceSpecificRequest, reply *struc } } + // If we're doing a connect query, we need read access to the service + // we're trying to find proxies for, so check that. + if args.Connect { + // Fetch the ACL token, if any. + rule, err := h.srv.resolveToken(args.Token) + if err != nil { + return err + } + + if rule != nil && !rule.ServiceRead(args.ServiceName) { + // Just return nil, which will return an empty response (tested) + return nil + } + } + err := h.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, diff --git a/agent/consul/health_endpoint_test.go b/agent/consul/health_endpoint_test.go index c9581e3a75..117afd6469 100644 --- a/agent/consul/health_endpoint_test.go +++ b/agent/consul/health_endpoint_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/net-rpc-msgpackrpc" + "github.com/stretchr/testify/assert" ) func TestHealth_ChecksInState(t *testing.T) { @@ -821,6 +822,106 @@ func TestHealth_ServiceNodes_DistanceSort(t *testing.T) { } } +func TestHealth_ServiceNodes_ConnectProxy_ACL(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + c.ACLEnforceVersion8 = false + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Create the ACL. + arg := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: ` +service "foo" { + policy = "write" +} +`, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + var token string + assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", arg, &token)) + + { + var out struct{} + + // Register a service + args := structs.TestRegisterRequestProxy(t) + args.WriteRequest.Token = "root" + args.Service.ID = "foo-proxy-0" + args.Service.Service = "foo-proxy" + args.Service.ProxyDestination = "bar" + args.Check = &structs.HealthCheck{ + Name: "proxy", + Status: api.HealthPassing, + ServiceID: args.Service.ID, + } + assert.Nil(msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out)) + + // Register a service + args = structs.TestRegisterRequestProxy(t) + args.WriteRequest.Token = "root" + args.Service.Service = "foo-proxy" + args.Service.ProxyDestination = "foo" + args.Check = &structs.HealthCheck{ + Name: "proxy", + Status: api.HealthPassing, + ServiceID: args.Service.Service, + } + assert.Nil(msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out)) + + // Register a service + args = structs.TestRegisterRequestProxy(t) + args.WriteRequest.Token = "root" + args.Service.Service = "another-proxy" + args.Service.ProxyDestination = "foo" + args.Check = &structs.HealthCheck{ + Name: "proxy", + Status: api.HealthPassing, + ServiceID: args.Service.Service, + } + assert.Nil(msgpackrpc.CallWithCodec(codec, "Catalog.Register", &args, &out)) + } + + // List w/ token. This should disallow because we don't have permission + // to read "bar" + req := structs.ServiceSpecificRequest{ + Connect: true, + Datacenter: "dc1", + ServiceName: "bar", + QueryOptions: structs.QueryOptions{Token: token}, + } + var resp structs.IndexedCheckServiceNodes + assert.Nil(msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &resp)) + assert.Len(resp.Nodes, 0) + + // List w/ token. This should work since we're requesting "foo", but should + // also only contain the proxies with names that adhere to our ACL. + req = structs.ServiceSpecificRequest{ + Connect: true, + Datacenter: "dc1", + ServiceName: "foo", + QueryOptions: structs.QueryOptions{Token: token}, + } + assert.Nil(msgpackrpc.CallWithCodec(codec, "Health.ServiceNodes", &req, &resp)) + assert.Len(resp.Nodes, 1) +} + func TestHealth_NodeChecks_FilterACL(t *testing.T) { t.Parallel() dir, token, srv, codec := testACLFilterServer(t)