mirror of https://github.com/hashicorp/consul
NET-11737 - sec vulnerability - remediate ability to use bexpr to filter results without ACL read on endpoint (#21950)
* NET-11737 - sec vulnerability - remediate ability to use bexpr to filter results without ACL read on endpoint * add changelog * update test descriptions to make more sensepull/21932/head^2
parent
21cca2dc5b
commit
3c3bdba926
@ -0,0 +1,3 @@
|
||||
```release-note:security
|
||||
Removed ability to use bexpr to filter results without ACL read on endpoint
|
||||
```
|
@ -1,109 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
|
||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
)
|
||||
|
||||
type MetadataFilterableResources interface {
|
||||
GetMetadata() map[string]string
|
||||
}
|
||||
|
||||
// FilterResourcesByMetadata will use the provided go-bexpr based filter to
|
||||
// retain matching items from the provided slice.
|
||||
//
|
||||
// The only variables usable in the expressions are the metadata keys prefixed
|
||||
// by "metadata."
|
||||
//
|
||||
// If no filter is provided, then this does nothing and returns the input.
|
||||
func FilterResourcesByMetadata[T MetadataFilterableResources](resources []T, filter string) ([]T, error) {
|
||||
if filter == "" || len(resources) == 0 {
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
eval, err := createMetadataFilterEvaluator(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]T, 0, len(resources))
|
||||
for _, res := range resources {
|
||||
vars := &metadataFilterFieldDetails{
|
||||
Meta: res.GetMetadata(),
|
||||
}
|
||||
match, err := eval.Evaluate(vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if match {
|
||||
filtered = append(filtered, res)
|
||||
}
|
||||
}
|
||||
if len(filtered) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// FilterMatchesResourceMetadata will use the provided go-bexpr based filter to
|
||||
// determine if the provided resource matches.
|
||||
//
|
||||
// The only variables usable in the expressions are the metadata keys prefixed
|
||||
// by "metadata."
|
||||
//
|
||||
// If no filter is provided, then this returns true.
|
||||
func FilterMatchesResourceMetadata(res *pbresource.Resource, filter string) (bool, error) {
|
||||
if res == nil {
|
||||
return false, nil
|
||||
} else if filter == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
eval, err := createMetadataFilterEvaluator(filter)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
vars := &metadataFilterFieldDetails{
|
||||
Meta: res.Metadata,
|
||||
}
|
||||
match, err := eval.Evaluate(vars)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return match, nil
|
||||
}
|
||||
|
||||
// ValidateMetadataFilter will validate that the provided filter is going to be
|
||||
// a valid input to the FilterResourcesByMetadata function.
|
||||
//
|
||||
// This is best called from a Validate hook.
|
||||
func ValidateMetadataFilter(filter string) error {
|
||||
if filter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := createMetadataFilterEvaluator(filter)
|
||||
return err
|
||||
}
|
||||
|
||||
func createMetadataFilterEvaluator(filter string) (*bexpr.Evaluator, error) {
|
||||
sampleVars := &metadataFilterFieldDetails{
|
||||
Meta: make(map[string]string),
|
||||
}
|
||||
eval, err := bexpr.CreateEvaluatorForType(filter, nil, sampleVars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("filter %q is invalid: %w", filter, err)
|
||||
}
|
||||
return eval, nil
|
||||
}
|
||||
|
||||
type metadataFilterFieldDetails struct {
|
||||
Meta map[string]string `bexpr:"metadata"`
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package resource
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||
"github.com/hashicorp/consul/proto/private/prototest"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
)
|
||||
|
||||
func TestFilterResourcesByMetadata(t *testing.T) {
|
||||
type testcase struct {
|
||||
in []*pbresource.Resource
|
||||
filter string
|
||||
expect []*pbresource.Resource
|
||||
expectErr string
|
||||
}
|
||||
|
||||
create := func(name string, kvs ...string) *pbresource.Resource {
|
||||
require.True(t, len(kvs)%2 == 0)
|
||||
|
||||
meta := make(map[string]string)
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
meta[kvs[i]] = kvs[i+1]
|
||||
}
|
||||
|
||||
return &pbresource.Resource{
|
||||
Id: &pbresource.ID{
|
||||
Name: name,
|
||||
},
|
||||
Metadata: meta,
|
||||
}
|
||||
}
|
||||
|
||||
run := func(t *testing.T, tc testcase) {
|
||||
got, err := FilterResourcesByMetadata(tc.in, tc.filter)
|
||||
if tc.expectErr != "" {
|
||||
require.Error(t, err)
|
||||
testutil.RequireErrorContains(t, err, tc.expectErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
prototest.AssertDeepEqual(t, tc.expect, got)
|
||||
}
|
||||
}
|
||||
|
||||
cases := map[string]testcase{
|
||||
"nil input": {},
|
||||
"no filter": {
|
||||
in: []*pbresource.Resource{
|
||||
create("one"),
|
||||
create("two"),
|
||||
create("three"),
|
||||
create("four"),
|
||||
},
|
||||
filter: "",
|
||||
expect: []*pbresource.Resource{
|
||||
create("one"),
|
||||
create("two"),
|
||||
create("three"),
|
||||
create("four"),
|
||||
},
|
||||
},
|
||||
"bad filter": {
|
||||
in: []*pbresource.Resource{
|
||||
create("one"),
|
||||
create("two"),
|
||||
create("three"),
|
||||
create("four"),
|
||||
},
|
||||
filter: "garbage.value == zzz",
|
||||
expectErr: `Selector "garbage" is not valid`,
|
||||
},
|
||||
"filter everything out": {
|
||||
in: []*pbresource.Resource{
|
||||
create("one"),
|
||||
create("two"),
|
||||
create("three"),
|
||||
create("four"),
|
||||
},
|
||||
filter: "metadata.foo == bar",
|
||||
},
|
||||
"filter simply": {
|
||||
in: []*pbresource.Resource{
|
||||
create("one", "foo", "bar"),
|
||||
create("two", "foo", "baz"),
|
||||
create("three", "zim", "gir"),
|
||||
create("four", "zim", "gaz", "foo", "bar"),
|
||||
},
|
||||
filter: "metadata.foo == bar",
|
||||
expect: []*pbresource.Resource{
|
||||
create("one", "foo", "bar"),
|
||||
create("four", "zim", "gaz", "foo", "bar"),
|
||||
},
|
||||
},
|
||||
"filter prefix": {
|
||||
in: []*pbresource.Resource{
|
||||
create("one", "foo", "bar"),
|
||||
create("two", "foo", "baz"),
|
||||
create("three", "zim", "gir"),
|
||||
create("four", "zim", "gaz", "foo", "bar"),
|
||||
create("four", "zim", "zzz"),
|
||||
},
|
||||
filter: "(zim in metadata) and (metadata.zim matches `^g.`)",
|
||||
expect: []*pbresource.Resource{
|
||||
create("three", "zim", "gir"),
|
||||
create("four", "zim", "gaz", "foo", "bar"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
run(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterMatchesResourceMetadata(t *testing.T) {
|
||||
type testcase struct {
|
||||
res *pbresource.Resource
|
||||
filter string
|
||||
expect bool
|
||||
expectErr string
|
||||
}
|
||||
|
||||
create := func(name string, kvs ...string) *pbresource.Resource {
|
||||
require.True(t, len(kvs)%2 == 0)
|
||||
|
||||
meta := make(map[string]string)
|
||||
for i := 0; i < len(kvs); i += 2 {
|
||||
meta[kvs[i]] = kvs[i+1]
|
||||
}
|
||||
|
||||
return &pbresource.Resource{
|
||||
Id: &pbresource.ID{
|
||||
Name: name,
|
||||
},
|
||||
Metadata: meta,
|
||||
}
|
||||
}
|
||||
|
||||
run := func(t *testing.T, tc testcase) {
|
||||
got, err := FilterMatchesResourceMetadata(tc.res, tc.filter)
|
||||
if tc.expectErr != "" {
|
||||
require.Error(t, err)
|
||||
testutil.RequireErrorContains(t, err, tc.expectErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expect, got)
|
||||
}
|
||||
}
|
||||
|
||||
cases := map[string]testcase{
|
||||
"nil input": {},
|
||||
"no filter": {
|
||||
res: create("one"),
|
||||
filter: "",
|
||||
expect: true,
|
||||
},
|
||||
"bad filter": {
|
||||
res: create("one"),
|
||||
filter: "garbage.value == zzz",
|
||||
expectErr: `Selector "garbage" is not valid`,
|
||||
},
|
||||
"no match": {
|
||||
res: create("one"),
|
||||
filter: "metadata.foo == bar",
|
||||
},
|
||||
"match simply": {
|
||||
res: create("one", "foo", "bar"),
|
||||
filter: "metadata.foo == bar",
|
||||
expect: true,
|
||||
},
|
||||
"match via prefix": {
|
||||
res: create("four", "zim", "gaz", "foo", "bar"),
|
||||
filter: "(zim in metadata) and (metadata.zim matches `^g.`)",
|
||||
expect: true,
|
||||
},
|
||||
"no match via prefix": {
|
||||
res: create("four", "zim", "zzz", "foo", "bar"),
|
||||
filter: "(zim in metadata) and (metadata.zim matches `^g.`)",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
run(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in new issue