mirror of https://github.com/hashicorp/consul
NET-11737 - sec vulnerability - remediate ability to use bexpr to filter results without ACL read on endpoint
parent
6662e48363
commit
07a618b1fc
@ -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