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:
parent
72e2695986
commit
42f02e80c3
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
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.<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
|
||||
|
||||
This endpoint updates an intention with the given values.
|
||||
|
|
Loading…
Reference in New Issue