From 42f02e80c3cfa7025424ef6de54e44abb0ceb9f3 Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Tue, 7 Apr 2020 11:48:44 -0400 Subject: [PATCH] =?UTF-8?q?Enable=20filtering=20language=20support=20for?= =?UTF-8?q?=20the=20v1/connect/intentions=E2=80=A6=20(#7593)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Enable filtering language support for the v1/connect/intentions listing API * Update website for filtering of Intentions * Update website/source/api/connect/intentions.html.md --- agent/consul/intention_endpoint.go | 18 ++++- agent/consul/intention_endpoint_test.go | 78 ++++++++----------- agent/structs/intention.go | 6 +- agent/structs/structs_filtering_test.go | 68 ++++++++++++++++ website/source/api/connect/intentions.html.md | 28 ++++++- 5 files changed, 149 insertions(+), 49 deletions(-) diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index f4e99e744..676632c4f 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" + "github.com/hashicorp/go-bexpr" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" ) @@ -290,6 +291,11 @@ func (s *Intention) List( return err } + filter, err := bexpr.CreateFilter(args.Filter, nil, reply.Intentions) + if err != nil { + return err + } + return s.srv.blockingQuery( &args.QueryOptions, &reply.QueryMeta, func(ws memdb.WatchSet, state *state.Store) error { @@ -303,7 +309,17 @@ func (s *Intention) List( reply.Intentions = make(structs.Intentions, 0) } - return s.srv.filterACL(args.Token, reply) + if err := s.srv.filterACL(args.Token, reply); err != nil { + return err + } + + raw, err := filter.Execute(reply.Intentions) + if err != nil { + return err + } + reply.Intentions = raw.(structs.Intentions) + + return nil }, ) } diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 2a6113545..0949e3f54 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -1062,41 +1062,17 @@ func TestIntentionList(t *testing.T) { func TestIntentionList_acl(t *testing.T) { t.Parallel() - assert := assert.New(t) - dir1, s1 := testServerWithConfig(t, func(c *Config) { - c.ACLDatacenter = "dc1" - c.ACLsEnabled = true - c.ACLMasterToken = "root" - c.ACLDefaultPolicy = "deny" - }) + dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil)) defer os.RemoveAll(dir1) defer s1.Shutdown() codec := rpcClient(t, s1) defer codec.Close() testrpc.WaitForLeader(t, s1.RPC, "dc1") + waitForNewACLs(t, s1) - // Create an ACL with service write permissions. This will grant - // intentions read. - var token string - { - var rules = ` -service "foo" { - policy = "write" -}` - - req := structs.ACLRequest{ - Datacenter: "dc1", - Op: structs.ACLSet, - ACL: structs.ACL{ - Name: "User token", - Type: structs.ACLTokenTypeClient, - Rules: rules, - }, - WriteRequest: structs.WriteRequest{Token: "root"}, - } - assert.Nil(msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token)) - } + token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service_prefix "foo" { policy = "write" }`) + require.NoError(t, err) // Create a few records for _, name := range []string{"foobar", "bar", "baz"} { @@ -1108,44 +1084,58 @@ service "foo" { ixn.Intention.SourceNS = "default" ixn.Intention.DestinationNS = "default" ixn.Intention.DestinationName = name - ixn.WriteRequest.Token = "root" + ixn.WriteRequest.Token = TestDefaultMasterToken // Create var reply string - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply)) } // Test with no token - { + t.Run("no-token", func(t *testing.T) { req := &structs.DCSpecificRequest{ Datacenter: "dc1", } var resp structs.IndexedIntentions - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) - assert.Len(resp.Intentions, 0) - } + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) + require.Len(t, resp.Intentions, 0) + }) // Test with management token - { + t.Run("master-token", func(t *testing.T) { req := &structs.DCSpecificRequest{ Datacenter: "dc1", - QueryOptions: structs.QueryOptions{Token: "root"}, + QueryOptions: structs.QueryOptions{Token: TestDefaultMasterToken}, } var resp structs.IndexedIntentions - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) - assert.Len(resp.Intentions, 3) - } + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) + require.Len(t, resp.Intentions, 3) + }) // Test with user token - { + t.Run("user-token", func(t *testing.T) { req := &structs.DCSpecificRequest{ Datacenter: "dc1", - QueryOptions: structs.QueryOptions{Token: token}, + QueryOptions: structs.QueryOptions{Token: token.SecretID}, } var resp structs.IndexedIntentions - assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) - assert.Len(resp.Intentions, 1) - } + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) + require.Len(t, resp.Intentions, 1) + }) + + t.Run("filtered", func(t *testing.T) { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{ + Token: TestDefaultMasterToken, + Filter: "DestinationName == foobar", + }, + } + + var resp structs.IndexedIntentions + require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) + require.Len(t, resp.Intentions, 1) + }) } // Test basic matching. We don't need to exhaustively test inputs since this diff --git a/agent/structs/intention.go b/agent/structs/intention.go index c966d856e..4e5ef42dd 100644 --- a/agent/structs/intention.go +++ b/agent/structs/intention.go @@ -71,16 +71,16 @@ type Intention struct { // CreatedAt and UpdatedAt keep track of when this record was created // or modified. - CreatedAt, UpdatedAt time.Time `mapstructure:"-"` + CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"` // Hash of the contents of the intention // // This is needed mainly for replication purposes. When replicating from // one DC to another keeping the content Hash will allow us to detect // content changes more efficiently than checking every single field - Hash []byte + Hash []byte `bexpr:"-"` - RaftIndex + RaftIndex `bexpr:"-"` } func (t *Intention) UnmarshalJSON(data []byte) (err error) { diff --git a/agent/structs/structs_filtering_test.go b/agent/structs/structs_filtering_test.go index c4473524b..63ecf0a14 100644 --- a/agent/structs/structs_filtering_test.go +++ b/agent/structs/structs_filtering_test.go @@ -525,6 +525,70 @@ var expectedFieldConfigNodeInfo bexpr.FieldConfigurations = bexpr.FieldConfigura }, } +var expectedFieldConfigIntention bexpr.FieldConfigurations = bexpr.FieldConfigurations{ + "ID": &bexpr.FieldConfiguration{ + StructFieldName: "ID", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "Description": &bexpr.FieldConfiguration{ + StructFieldName: "Description", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "SourceNS": &bexpr.FieldConfiguration{ + StructFieldName: "SourceNS", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "SourceName": &bexpr.FieldConfiguration{ + StructFieldName: "SourceName", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "DestinationNS": &bexpr.FieldConfiguration{ + StructFieldName: "DestinationNS", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "DestinationName": &bexpr.FieldConfiguration{ + StructFieldName: "DestinationName", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "SourceType": &bexpr.FieldConfiguration{ + StructFieldName: "SourceType", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "Action": &bexpr.FieldConfiguration{ + StructFieldName: "Action", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "DefaultAddr": &bexpr.FieldConfiguration{ + StructFieldName: "DefaultAddr", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual, bexpr.MatchIn, bexpr.MatchNotIn, bexpr.MatchMatches, bexpr.MatchNotMatches}, + }, + "DefaultPort": &bexpr.FieldConfiguration{ + StructFieldName: "DefaultPort", + CoerceFn: bexpr.CoerceInt, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual}, + }, + "Precedence": &bexpr.FieldConfiguration{ + StructFieldName: "Precedence", + CoerceFn: bexpr.CoerceInt, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchEqual, bexpr.MatchNotEqual}, + }, + "Meta": &bexpr.FieldConfiguration{ + StructFieldName: "Meta", + CoerceFn: bexpr.CoerceString, + SupportedOperations: []bexpr.MatchOperator{bexpr.MatchIsEmpty, bexpr.MatchIsNotEmpty, bexpr.MatchIn, bexpr.MatchNotIn}, + SubFields: expectedFieldConfigMapStringValue, + }, +} + // Only need to generate the field configurations for the top level filtered types // The internal types will be checked within these. var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{ @@ -558,6 +622,10 @@ var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{ // registered with an agent stays in sync with our internal NodeService structure expected: expectedFieldConfigNodeService, }, + "Intention": fieldConfigTest{ + dataType: (*Intention)(nil), + expected: expectedFieldConfigIntention, + }, } func validateFieldConfigurationsRecurse(t *testing.T, expected, actual bexpr.FieldConfigurations, path string) bool { diff --git a/website/source/api/connect/intentions.html.md b/website/source/api/connect/intentions.html.md index 2cde186f0..c7bc80b82 100644 --- a/website/source/api/connect/intentions.html.md +++ b/website/source/api/connect/intentions.html.md @@ -165,11 +165,16 @@ The table below shows this endpoint's support for 1 Intention ACL rules are specified as part of a `service` rule. See [Intention Management Permissions](/docs/connect/intentions.html#intention-management-permissions) for more details. +### Parameters + +- `filter` `(string: "")` - Specifies the expression used to filter the + queries results prior to returning the data. + ### Sample Request ```text $ curl \ - http://127.0.0.1:8500/v1/connect/intentions + 'http://127.0.0.1:8500/v1/connect/intentions?filter=SourceName==web' ``` ### Sample Response @@ -197,6 +202,27 @@ $ curl \ ] ``` +### Filtering + +The filter will be executed against each Intention in the result list with +the following selectors and filter operations being supported: + +| Selector | Supported Operations | +| --------------- | -------------------------------------------------- | +| Action | Equal, Not Equal, In, Not In, Matches, Not Matches | +| DefaultAddr | Equal, Not Equal, In, Not In, Matches, Not Matches | +| DefaultPort | Equal, Not Equal | +| Description | Equal, Not Equal, In, Not In, Matches, Not Matches | +| DestinationNS | Equal, Not Equal, In, Not In, Matches, Not Matches | +| DestinationName | Equal, Not Equal, In, Not In, Matches, Not Matches | +| ID | Equal, Not Equal, In, Not In, Matches, Not Matches | +| Meta | Is Empty, Is Not Empty, In, Not In | +| Meta. | Equal, Not Equal, In, Not In, Matches, Not Matches | +| Precedence | Equal, Not Equal | +| SourceNS | Equal, Not Equal, In, Not In, Matches, Not Matches | +| SourceName | Equal, Not Equal, In, Not In, Matches, Not Matches | +| SourceType | Equal, Not Equal, In, Not In, Matches, Not Matches | + ## Update Intention This endpoint updates an intention with the given values.