Enable filtering language support for the v1/connect/intentions… (#7593)

* 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
This commit is contained in:
Matt Keeler 2020-04-07 11:48:44 -04:00 committed by GitHub
parent 8549cc2d99
commit 0e7d3d93b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 149 additions and 49 deletions

View File

@ -11,6 +11,7 @@ import (
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-bexpr"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
) )
@ -290,6 +291,11 @@ func (s *Intention) List(
return err return err
} }
filter, err := bexpr.CreateFilter(args.Filter, nil, reply.Intentions)
if err != nil {
return err
}
return s.srv.blockingQuery( return s.srv.blockingQuery(
&args.QueryOptions, &reply.QueryMeta, &args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error { func(ws memdb.WatchSet, state *state.Store) error {
@ -303,7 +309,17 @@ func (s *Intention) List(
reply.Intentions = make(structs.Intentions, 0) 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
}, },
) )
} }

View File

@ -1062,41 +1062,17 @@ func TestIntentionList(t *testing.T) {
func TestIntentionList_acl(t *testing.T) { func TestIntentionList_acl(t *testing.T) {
t.Parallel() t.Parallel()
assert := assert.New(t) dir1, s1 := testServerWithConfig(t, testServerACLConfig(nil))
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLMasterToken = "root"
c.ACLDefaultPolicy = "deny"
})
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)
defer codec.Close() defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1") testrpc.WaitForLeader(t, s1.RPC, "dc1")
waitForNewACLs(t, s1)
// Create an ACL with service write permissions. This will grant token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service_prefix "foo" { policy = "write" }`)
// intentions read. require.NoError(t, err)
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))
}
// Create a few records // Create a few records
for _, name := range []string{"foobar", "bar", "baz"} { for _, name := range []string{"foobar", "bar", "baz"} {
@ -1108,44 +1084,58 @@ service "foo" {
ixn.Intention.SourceNS = "default" ixn.Intention.SourceNS = "default"
ixn.Intention.DestinationNS = "default" ixn.Intention.DestinationNS = "default"
ixn.Intention.DestinationName = name ixn.Intention.DestinationName = name
ixn.WriteRequest.Token = "root" ixn.WriteRequest.Token = TestDefaultMasterToken
// Create // Create
var reply string 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 // Test with no token
{ t.Run("no-token", func(t *testing.T) {
req := &structs.DCSpecificRequest{ req := &structs.DCSpecificRequest{
Datacenter: "dc1", Datacenter: "dc1",
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
assert.Len(resp.Intentions, 0) require.Len(t, resp.Intentions, 0)
} })
// Test with management token // Test with management token
{ t.Run("master-token", func(t *testing.T) {
req := &structs.DCSpecificRequest{ req := &structs.DCSpecificRequest{
Datacenter: "dc1", Datacenter: "dc1",
QueryOptions: structs.QueryOptions{Token: "root"}, QueryOptions: structs.QueryOptions{Token: TestDefaultMasterToken},
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
assert.Len(resp.Intentions, 3) require.Len(t, resp.Intentions, 3)
} })
// Test with user token // Test with user token
{ t.Run("user-token", func(t *testing.T) {
req := &structs.DCSpecificRequest{ req := &structs.DCSpecificRequest{
Datacenter: "dc1", Datacenter: "dc1",
QueryOptions: structs.QueryOptions{Token: token}, QueryOptions: structs.QueryOptions{Token: token.SecretID},
} }
var resp structs.IndexedIntentions var resp structs.IndexedIntentions
assert.Nil(msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp)) require.NoError(t, msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp))
assert.Len(resp.Intentions, 1) 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 // Test basic matching. We don't need to exhaustively test inputs since this

View File

@ -71,16 +71,16 @@ type Intention struct {
// CreatedAt and UpdatedAt keep track of when this record was created // CreatedAt and UpdatedAt keep track of when this record was created
// or modified. // or modified.
CreatedAt, UpdatedAt time.Time `mapstructure:"-"` CreatedAt, UpdatedAt time.Time `mapstructure:"-" bexpr:"-"`
// Hash of the contents of the intention // Hash of the contents of the intention
// //
// This is needed mainly for replication purposes. When replicating from // This is needed mainly for replication purposes. When replicating from
// one DC to another keeping the content Hash will allow us to detect // one DC to another keeping the content Hash will allow us to detect
// content changes more efficiently than checking every single field // 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) { func (t *Intention) UnmarshalJSON(data []byte) (err error) {

View File

@ -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 // Only need to generate the field configurations for the top level filtered types
// The internal types will be checked within these. // The internal types will be checked within these.
var fieldConfigTests map[string]fieldConfigTest = map[string]fieldConfigTest{ 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 // registered with an agent stays in sync with our internal NodeService structure
expected: expectedFieldConfigNodeService, expected: expectedFieldConfigNodeService,
}, },
"Intention": fieldConfigTest{
dataType: (*Intention)(nil),
expected: expectedFieldConfigIntention,
},
} }
func validateFieldConfigurationsRecurse(t *testing.T, expected, actual bexpr.FieldConfigurations, path string) bool { func validateFieldConfigurationsRecurse(t *testing.T, expected, actual bexpr.FieldConfigurations, path string) bool {

View File

@ -165,11 +165,16 @@ The table below shows this endpoint's support for
<sup>1</sup> Intention ACL rules are specified as part of a `service` rule. <sup>1</sup> 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. 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 ### Sample Request
```text ```text
$ curl \ $ curl \
http://127.0.0.1:8500/v1/connect/intentions 'http://127.0.0.1:8500/v1/connect/intentions?filter=SourceName==web'
``` ```
### Sample Response ### 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.<any> | 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 ## Update Intention
This endpoint updates an intention with the given values. This endpoint updates an intention with the given values.