diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go
index f4e99e7441..676632c4f0 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 2a61135456..0949e3f546 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 c966d856e7..4e5ef42dd3 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 c4473524b7..63ecf0a14b 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 2cde186f05..c7bc80b822 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.