mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
event stream: fix wildcard namespace bypass (#25089)
This commit is contained in:
committed by
GitHub
parent
92c90af542
commit
c4f232f23e
3
.changelog/25089.txt
Normal file
3
.changelog/25089.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:security
|
||||||
|
event stream: fixes vulnerability CVE-2025-0937, where using a wildcard namespace to subscribe to the events API grants a user with "read" capabilites on any namespace, the ability to read events from all namespaces.
|
||||||
|
```
|
||||||
@@ -41,6 +41,10 @@ func (e *Event) stream(conn io.ReadWriteCloser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authErr := e.srv.Authenticate(nil, &args)
|
authErr := e.srv.Authenticate(nil, &args)
|
||||||
|
if authErr != nil {
|
||||||
|
handleJsonResultError(structs.ErrPermissionDenied, pointer.Of(int64(403)), encoder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// forward to appropriate region
|
// forward to appropriate region
|
||||||
if args.Region != e.srv.config.Region {
|
if args.Region != e.srv.config.Region {
|
||||||
@@ -52,16 +56,27 @@ func (e *Event) stream(conn io.ReadWriteCloser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.srv.MeasureRPCRate("event", structs.RateMetricRead, &args)
|
e.srv.MeasureRPCRate("event", structs.RateMetricRead, &args)
|
||||||
if authErr != nil {
|
|
||||||
|
resolvedACL, err := e.srv.ResolveACL(&args)
|
||||||
|
if err != nil {
|
||||||
handleJsonResultError(structs.ErrPermissionDenied, pointer.Of(int64(403)), encoder)
|
handleJsonResultError(structs.ErrPermissionDenied, pointer.Of(int64(403)), encoder)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedNses, err := e.validateACL(args.Namespace, args.Topics, resolvedACL)
|
||||||
|
if err != nil {
|
||||||
|
handleJsonResultError(structs.ErrPermissionDenied, pointer.Of(int64(403)), encoder)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the subscription request
|
// Generate the subscription request
|
||||||
subReq := &stream.SubscribeRequest{
|
subReq := &stream.SubscribeRequest{
|
||||||
Token: args.AuthToken,
|
Token: args.AuthToken,
|
||||||
Topics: args.Topics,
|
Topics: args.Topics,
|
||||||
Index: uint64(args.Index),
|
Index: uint64(args.Index),
|
||||||
Namespace: args.Namespace,
|
// Namespaces is set once, in the event a users ACL is updated to include
|
||||||
|
// more NSes, the current event stream will not include the new NSes.
|
||||||
|
Namespaces: validatedNses,
|
||||||
Authenticate: func() error {
|
Authenticate: func() error {
|
||||||
if err := e.srv.Authenticate(nil, &args); err != nil {
|
if err := e.srv.Authenticate(nil, &args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -70,7 +85,8 @@ func (e *Event) stream(conn io.ReadWriteCloser) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return validateACL(args.Namespace, args.Topics, resolvedACL)
|
_, err = e.validateACL(args.Namespace, args.Topics, resolvedACL)
|
||||||
|
return err
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +241,26 @@ func handleJsonResultError(err error, code *int64, encoder *codec.Encoder) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateACL(namespace string, topics map[structs.Topic][]string, aclObj *acl.ACL) error {
|
// validateACL handles wildcard namespaces by replacing it with all existing namespaces
|
||||||
|
// and validates the user has the appropriate ACL to read topics in each one.
|
||||||
|
func (e *Event) validateACL(namespace string, topics map[structs.Topic][]string, resolvedAcl *acl.ACL) ([]string, error) {
|
||||||
|
nses := []string{}
|
||||||
|
if namespace == structs.AllNamespacesSentinel {
|
||||||
|
ns, _ := e.srv.State().NamespaceNames()
|
||||||
|
nses = append(nses, ns...)
|
||||||
|
} else {
|
||||||
|
nses = append(nses, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range nses {
|
||||||
|
if err := validateNsOp(ns, topics, resolvedAcl); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNsOp(namespace string, topics map[structs.Topic][]string, aclObj *acl.ACL) error {
|
||||||
for topic := range topics {
|
for topic := range topics {
|
||||||
switch topic {
|
switch topic {
|
||||||
case structs.TopicDeployment,
|
case structs.TopicDeployment,
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ OUTER:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEventStream_validateACL(t *testing.T) {
|
func TestEventStream_validateNsOp(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
@@ -480,12 +480,74 @@ func TestEventStream_validateACL(t *testing.T) {
|
|||||||
testACL, err := acl.NewACL(tc.Management, []*acl.Policy{p})
|
testACL, err := acl.NewACL(tc.Management, []*acl.Policy{p})
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
|
||||||
err = validateACL(tc.Namespace, tc.Topics, testACL)
|
err = validateNsOp(tc.Namespace, tc.Topics, testACL)
|
||||||
require.Equal(tc.ExpectedErr, err)
|
require.Equal(tc.ExpectedErr, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEventStream_validateACL(t *testing.T) {
|
||||||
|
ci.Parallel(t)
|
||||||
|
|
||||||
|
s1, _, cleanupS := TestACLServer(t, nil)
|
||||||
|
defer cleanupS()
|
||||||
|
testutil.WaitForLeader(t, s1.RPC)
|
||||||
|
|
||||||
|
ns1 := mock.Namespace()
|
||||||
|
|
||||||
|
err := s1.State().UpsertNamespaces(0, []*structs.Namespace{ns1})
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
testEvent := &Event{srv: s1}
|
||||||
|
|
||||||
|
t.Run("single namespace ACL errors on wildcard", func(t *testing.T) {
|
||||||
|
policy, err := acl.Parse(mock.NamespacePolicy(ns1.Name, "", []string{acl.NamespaceCapabilityReadJob}))
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
// does not contain policy for default NS
|
||||||
|
testAcl, err := acl.NewACL(false, []*acl.Policy{policy})
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
topics := map[structs.Topic][]string{
|
||||||
|
structs.TopicJob: {"*"},
|
||||||
|
}
|
||||||
|
_, err = testEvent.validateACL("*", topics, testAcl)
|
||||||
|
must.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all namespace ACL succeeds on wildcard", func(t *testing.T) {
|
||||||
|
policy1, err := acl.Parse(mock.NamespacePolicy("default", "", []string{acl.NamespaceCapabilityReadJob}))
|
||||||
|
must.NoError(t, err)
|
||||||
|
policy2, err := acl.Parse(mock.NamespacePolicy(ns1.Name, "", []string{acl.NamespaceCapabilityReadJob}))
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
testAcl, err := acl.NewACL(false, []*acl.Policy{policy1, policy2})
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
topics := map[structs.Topic][]string{
|
||||||
|
structs.TopicJob: {"*"},
|
||||||
|
}
|
||||||
|
nses, err := testEvent.validateACL("*", topics, testAcl)
|
||||||
|
must.NoError(t, err)
|
||||||
|
must.Eq(t, nses, []string{"default", ns1.Name})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single namespace ACL succeeds with correct NS", func(t *testing.T) {
|
||||||
|
policy, err := acl.Parse(mock.NamespacePolicy("default", "", []string{acl.NamespaceCapabilityReadJob}))
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
testAcl, err := acl.NewACL(false, []*acl.Policy{policy})
|
||||||
|
must.NoError(t, err)
|
||||||
|
|
||||||
|
topics := map[structs.Topic][]string{
|
||||||
|
structs.TopicJob: {"*"},
|
||||||
|
}
|
||||||
|
nses, err := testEvent.validateACL("default", topics, testAcl)
|
||||||
|
must.NoError(t, err)
|
||||||
|
must.Eq(t, nses, []string{"default"})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestEventStream_ACL_Update_Close_Stream asserts that an active subscription
|
// TestEventStream_ACL_Update_Close_Stream asserts that an active subscription
|
||||||
// is closed after the token is no longer valid
|
// is closed after the token is no longer valid
|
||||||
func TestEventStream_ACL_Update_Close_Stream(t *testing.T) {
|
func TestEventStream_ACL_Update_Close_Stream(t *testing.T) {
|
||||||
|
|||||||
@@ -3676,7 +3676,7 @@ func TestFSM_ACLEvents(t *testing.T) {
|
|||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
tc.reqTopic: {"*"},
|
tc.reqTopic: {"*"},
|
||||||
},
|
},
|
||||||
Namespace: "default",
|
Namespaces: []string{"default"},
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := broker.Subscribe(subReq)
|
sub, err := broker.Subscribe(subReq)
|
||||||
@@ -3730,7 +3730,7 @@ func TestFSM_EventBroker_JobRegisterFSMEvents(t *testing.T) {
|
|||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
structs.TopicJob: {"*"},
|
structs.TopicJob: {"*"},
|
||||||
},
|
},
|
||||||
Namespace: "default",
|
Namespaces: []string{"default"},
|
||||||
}
|
}
|
||||||
|
|
||||||
sub, err := broker.Subscribe(subReq)
|
sub, err := broker.Subscribe(subReq)
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func EventsForIndex(t *testing.T, s *StateStore, index uint64) []structs.Event {
|
|||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
"*": {"*"},
|
"*": {"*"},
|
||||||
},
|
},
|
||||||
Namespace: "default",
|
Namespaces: []string{"default"},
|
||||||
Index: index,
|
Index: index,
|
||||||
StartExactlyAtIndex: true,
|
StartExactlyAtIndex: true,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package stream
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"slices"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/hashicorp/nomad/nomad/structs"
|
"github.com/hashicorp/nomad/nomad/structs"
|
||||||
@@ -48,9 +49,9 @@ type Subscription struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SubscribeRequest struct {
|
type SubscribeRequest struct {
|
||||||
Token string
|
Token string
|
||||||
Index uint64
|
Index uint64
|
||||||
Namespace string
|
Namespaces []string
|
||||||
|
|
||||||
Topics map[structs.Topic][]string
|
Topics map[structs.Topic][]string
|
||||||
|
|
||||||
@@ -130,15 +131,10 @@ func filter(req *SubscribeRequest, events []structs.Event) []structs.Event {
|
|||||||
|
|
||||||
allTopicKeys := req.Topics[structs.TopicAll]
|
allTopicKeys := req.Topics[structs.TopicAll]
|
||||||
|
|
||||||
// Return all events if subscribed to all namespaces and all topics
|
|
||||||
if req.Namespace == "*" && len(allTopicKeys) == 1 && allTopicKeys[0] == string(structs.TopicAll) {
|
|
||||||
return events
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []structs.Event
|
var result []structs.Event
|
||||||
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
if req.Namespace != "*" && event.Namespace != "" && event.Namespace != req.Namespace {
|
if event.Namespace != "" && !slices.Contains(req.Namespaces, event.Namespace) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ func TestFilter_AllKeys(t *testing.T) {
|
|||||||
func TestFilter_PartialMatch_Topic(t *testing.T) {
|
func TestFilter_PartialMatch_Topic(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := make([]structs.Event, 0, 5)
|
event1 := structs.Event{Topic: "Test", Key: "One"}
|
||||||
events = append(events, structs.Event{Topic: "Test", Key: "One"}, structs.Event{Topic: "Test", Key: "Two"}, structs.Event{Topic: "Exclude", Key: "Two"})
|
event2 := structs.Event{Topic: "Test", Key: "Two"}
|
||||||
|
event3 := structs.Event{Topic: "Exclude", Key: "Two"}
|
||||||
|
events := []structs.Event{event1, event2, event3}
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
@@ -53,7 +55,7 @@ func TestFilter_PartialMatch_Topic(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{{Topic: "Test", Key: "One"}, {Topic: "Test", Key: "Two"}}
|
expected := []structs.Event{event1, event2}
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
|
|
||||||
require.Equal(t, 2, cap(actual))
|
require.Equal(t, 2, cap(actual))
|
||||||
@@ -62,11 +64,10 @@ func TestFilter_PartialMatch_Topic(t *testing.T) {
|
|||||||
func TestFilter_Match_TopicAll_SpecificKey(t *testing.T) {
|
func TestFilter_Match_TopicAll_SpecificKey(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := []structs.Event{
|
event1 := structs.Event{Topic: "Match", Key: "Two"}
|
||||||
{Topic: "Match", Key: "Two"},
|
event2 := structs.Event{Topic: "NoMatch", Key: "One"}
|
||||||
{Topic: "NoMatch", Key: "One"},
|
event3 := structs.Event{Topic: "OtherMatch", Key: "Two"}
|
||||||
{Topic: "OtherMatch", Key: "Two"},
|
events := []structs.Event{event1, event2, event3}
|
||||||
}
|
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
@@ -75,21 +76,17 @@ func TestFilter_Match_TopicAll_SpecificKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{
|
expected := []structs.Event{event1, event3}
|
||||||
{Topic: "Match", Key: "Two"},
|
|
||||||
{Topic: "OtherMatch", Key: "Two"},
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilter_Match_TopicAll_SpecificKey_Plus(t *testing.T) {
|
func TestFilter_Match_TopicAll_SpecificKey_Plus(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := []structs.Event{
|
event1 := structs.Event{Topic: "FirstTwo", Key: "Two"}
|
||||||
{Topic: "FirstTwo", Key: "Two"},
|
event2 := structs.Event{Topic: "Test", Key: "One"}
|
||||||
{Topic: "Test", Key: "One"},
|
event3 := structs.Event{Topic: "SecondTwo", Key: "Two"}
|
||||||
{Topic: "SecondTwo", Key: "Two"},
|
events := []structs.Event{event1, event2, event3}
|
||||||
}
|
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
@@ -99,19 +96,16 @@ func TestFilter_Match_TopicAll_SpecificKey_Plus(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{
|
expected := []structs.Event{event1, event2, event3}
|
||||||
{Topic: "FirstTwo", Key: "Two"},
|
|
||||||
{Topic: "Test", Key: "One"},
|
|
||||||
{Topic: "SecondTwo", Key: "Two"},
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilter_PartialMatch_Key(t *testing.T) {
|
func TestFilter_PartialMatch_Key(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := make([]structs.Event, 0, 5)
|
event1 := structs.Event{Topic: "Test", Key: "One"}
|
||||||
events = append(events, structs.Event{Topic: "Test", Key: "One"}, structs.Event{Topic: "Test", Key: "Two"})
|
event2 := structs.Event{Topic: "Test", Key: "Two"}
|
||||||
|
events := []structs.Event{event1, event2}
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
@@ -119,7 +113,7 @@ func TestFilter_PartialMatch_Key(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{{Topic: "Test", Key: "One"}}
|
expected := []structs.Event{event1}
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
|
|
||||||
require.Equal(t, 1, cap(actual))
|
require.Equal(t, 1, cap(actual))
|
||||||
@@ -147,66 +141,39 @@ func TestFilter_NoMatch(t *testing.T) {
|
|||||||
func TestFilter_Namespace(t *testing.T) {
|
func TestFilter_Namespace(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := make([]structs.Event, 0, 5)
|
event1 := structs.Event{Topic: "Test", Key: "One", Namespace: "foo"}
|
||||||
events = append(events, structs.Event{Topic: "Test", Key: "One", Namespace: "foo"}, structs.Event{Topic: "Test", Key: "Two"}, structs.Event{Topic: "Test", Key: "Two", Namespace: "bar"})
|
event2 := structs.Event{Topic: "Test", Key: "Two", Namespace: "foo"}
|
||||||
|
event3 := structs.Event{Topic: "Test", Key: "Two", Namespace: "bar"}
|
||||||
|
events := []structs.Event{event1, event2, event3}
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
"*": {"*"},
|
"*": {"*"},
|
||||||
},
|
},
|
||||||
Namespace: "foo",
|
Namespaces: []string{"foo"},
|
||||||
}
|
}
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{
|
// expect namespace "bar" to be filtered out
|
||||||
{Topic: "Test", Key: "One", Namespace: "foo"},
|
expected := []structs.Event{event1, event2}
|
||||||
{Topic: "Test", Key: "Two"},
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
|
|
||||||
require.Equal(t, 2, cap(actual))
|
require.Equal(t, 2, cap(actual))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilter_NamespaceAll(t *testing.T) {
|
|
||||||
ci.Parallel(t)
|
|
||||||
|
|
||||||
events := make([]structs.Event, 0, 5)
|
|
||||||
events = append(events,
|
|
||||||
structs.Event{Topic: "Test", Key: "One", Namespace: "foo"},
|
|
||||||
structs.Event{Topic: "Test", Key: "Two", Namespace: "bar"},
|
|
||||||
structs.Event{Topic: "Test", Key: "Three", Namespace: "default"},
|
|
||||||
)
|
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
|
||||||
Topics: map[structs.Topic][]string{
|
|
||||||
"*": {"*"},
|
|
||||||
},
|
|
||||||
Namespace: "*",
|
|
||||||
}
|
|
||||||
actual := filter(req, events)
|
|
||||||
expected := []structs.Event{
|
|
||||||
{Topic: "Test", Key: "One", Namespace: "foo"},
|
|
||||||
{Topic: "Test", Key: "Two", Namespace: "bar"},
|
|
||||||
{Topic: "Test", Key: "Three", Namespace: "default"},
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilter_FilterKeys(t *testing.T) {
|
func TestFilter_FilterKeys(t *testing.T) {
|
||||||
ci.Parallel(t)
|
ci.Parallel(t)
|
||||||
|
|
||||||
events := make([]structs.Event, 0, 5)
|
event1 := structs.Event{Topic: "Test", Key: "One", FilterKeys: []string{"extra-key"}}
|
||||||
events = append(events, structs.Event{Topic: "Test", Key: "One", FilterKeys: []string{"extra-key"}}, structs.Event{Topic: "Test", Key: "Two"}, structs.Event{Topic: "Test", Key: "Two"})
|
event2 := structs.Event{Topic: "Test", Key: "Two"}
|
||||||
|
event3 := structs.Event{Topic: "Test", Key: "Two"}
|
||||||
|
events := []structs.Event{event1, event2, event3}
|
||||||
|
|
||||||
req := &SubscribeRequest{
|
req := &SubscribeRequest{
|
||||||
Topics: map[structs.Topic][]string{
|
Topics: map[structs.Topic][]string{
|
||||||
"Test": {"extra-key"},
|
"Test": {"extra-key"},
|
||||||
},
|
},
|
||||||
Namespace: "foo",
|
|
||||||
}
|
}
|
||||||
actual := filter(req, events)
|
actual := filter(req, events)
|
||||||
expected := []structs.Event{
|
expected := []structs.Event{event1}
|
||||||
{Topic: "Test", Key: "One", FilterKeys: []string{"extra-key"}},
|
|
||||||
}
|
|
||||||
require.Equal(t, expected, actual)
|
require.Equal(t, expected, actual)
|
||||||
|
|
||||||
require.Equal(t, 1, cap(actual))
|
require.Equal(t, 1, cap(actual))
|
||||||
|
|||||||
Reference in New Issue
Block a user