mirror of https://github.com/hashicorp/consul
Browse Source
* 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 sensemain
John Murret
18 hours ago
committed by
GitHub
18 changed files with 1364 additions and 520 deletions
@ -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