github.com/hernad/nomad@v1.6.112/nomad/stream/event_broker_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package stream
     5  
     6  import (
     7  	"context"
     8  	"sync/atomic"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/hashicorp/go-memdb"
    13  	"github.com/hernad/nomad/acl"
    14  	"github.com/hernad/nomad/ci"
    15  	"github.com/hernad/nomad/helper/pointer"
    16  	"github.com/hernad/nomad/helper/uuid"
    17  	"github.com/hernad/nomad/nomad/mock"
    18  	"github.com/hernad/nomad/nomad/structs"
    19  	"github.com/shoenig/test/must"
    20  
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  func TestEventBroker_PublishChangesAndSubscribe(t *testing.T) {
    25  	ci.Parallel(t)
    26  
    27  	subscription := &SubscribeRequest{
    28  		Topics: map[structs.Topic][]string{
    29  			"Test": {"sub-key"},
    30  		},
    31  	}
    32  	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    33  	defer cancel()
    34  
    35  	publisher, err := NewEventBroker(ctx, nil, EventBrokerCfg{EventBufferSize: 100})
    36  	require.NoError(t, err)
    37  
    38  	sub, err := publisher.Subscribe(subscription)
    39  	require.NoError(t, err)
    40  	eventCh := consumeSubscription(ctx, sub)
    41  
    42  	// Now subscriber should block waiting for updates
    43  	assertNoResult(t, eventCh)
    44  
    45  	events := []structs.Event{{
    46  		Index:   1,
    47  		Topic:   "Test",
    48  		Key:     "sub-key",
    49  		Payload: "sample payload",
    50  	}}
    51  	publisher.Publish(&structs.Events{Index: 1, Events: events})
    52  
    53  	// Subscriber should see the published event
    54  	result := nextResult(t, eventCh)
    55  	require.NoError(t, result.Err)
    56  	expected := []structs.Event{{Payload: "sample payload", Key: "sub-key", Topic: "Test", Index: 1}}
    57  	require.Equal(t, expected, result.Events)
    58  
    59  	// Now subscriber should block waiting for updates
    60  	assertNoResult(t, eventCh)
    61  
    62  	// Publish a second event
    63  	events = []structs.Event{{
    64  		Index:   2,
    65  		Topic:   "Test",
    66  		Key:     "sub-key",
    67  		Payload: "sample payload 2",
    68  	}}
    69  	publisher.Publish(&structs.Events{Index: 2, Events: events})
    70  
    71  	result = nextResult(t, eventCh)
    72  	require.NoError(t, result.Err)
    73  	expected = []structs.Event{{Payload: "sample payload 2", Key: "sub-key", Topic: "Test", Index: 2}}
    74  	require.Equal(t, expected, result.Events)
    75  }
    76  
    77  func TestEventBroker_ShutdownClosesSubscriptions(t *testing.T) {
    78  	ci.Parallel(t)
    79  
    80  	ctx, cancel := context.WithCancel(context.Background())
    81  	t.Cleanup(cancel)
    82  
    83  	publisher, err := NewEventBroker(ctx, nil, EventBrokerCfg{})
    84  	require.NoError(t, err)
    85  
    86  	sub1, err := publisher.Subscribe(&SubscribeRequest{})
    87  	require.NoError(t, err)
    88  	defer sub1.Unsubscribe()
    89  
    90  	sub2, err := publisher.Subscribe(&SubscribeRequest{})
    91  	require.NoError(t, err)
    92  	defer sub2.Unsubscribe()
    93  
    94  	cancel() // Shutdown
    95  
    96  	err = consumeSub(context.Background(), sub1)
    97  	require.Equal(t, err, ErrSubscriptionClosed)
    98  
    99  	_, err = sub2.Next(context.Background())
   100  	require.Equal(t, err, ErrSubscriptionClosed)
   101  }
   102  
   103  // TestEventBroker_EmptyReqToken_DistinctSubscriptions tests subscription
   104  // hanlding behavior when ACLs are disabled (request Token is empty).
   105  // Subscriptions are mapped by their request token.  when that token is empty,
   106  // the subscriptions should still be handled indeppendtly of each other when
   107  // unssubscribing.
   108  func TestEventBroker_EmptyReqToken_DistinctSubscriptions(t *testing.T) {
   109  	ci.Parallel(t)
   110  
   111  	ctx, cancel := context.WithCancel(context.Background())
   112  	t.Cleanup(cancel)
   113  
   114  	publisher, err := NewEventBroker(ctx, nil, EventBrokerCfg{})
   115  	require.NoError(t, err)
   116  
   117  	// first subscription, empty token
   118  	sub1, err := publisher.Subscribe(&SubscribeRequest{})
   119  	require.NoError(t, err)
   120  	defer sub1.Unsubscribe()
   121  
   122  	// second subscription, empty token
   123  	sub2, err := publisher.Subscribe(&SubscribeRequest{})
   124  	require.NoError(t, err)
   125  	require.NotNil(t, sub2)
   126  
   127  	sub1.Unsubscribe()
   128  
   129  	require.Equal(t, subscriptionStateOpen, atomic.LoadUint32(&sub2.state))
   130  }
   131  
   132  func TestEventBroker_handleACLUpdates_TokenDeleted(t *testing.T) {
   133  	ci.Parallel(t)
   134  
   135  	ctx, cancel := context.WithCancel(context.Background())
   136  	t.Cleanup(cancel)
   137  
   138  	publisher, err := NewEventBroker(ctx, nil, EventBrokerCfg{})
   139  	require.NoError(t, err)
   140  
   141  	sub1, err := publisher.Subscribe(&SubscribeRequest{
   142  		Topics: map[structs.Topic][]string{
   143  			"*": {"*"},
   144  		},
   145  		Token: "foo",
   146  	})
   147  	require.NoError(t, err)
   148  	defer sub1.Unsubscribe()
   149  
   150  	aclEvent := structs.Event{
   151  		Topic:   structs.TopicACLToken,
   152  		Type:    structs.TypeACLTokenDeleted,
   153  		Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: "foo"}),
   154  	}
   155  
   156  	publisher.Publish(&structs.Events{Index: 100, Events: []structs.Event{aclEvent}})
   157  	for {
   158  		_, err := sub1.Next(ctx)
   159  		if err == ErrSubscriptionClosed {
   160  			break
   161  		}
   162  	}
   163  
   164  	out, err := sub1.Next(ctx)
   165  	require.Error(t, err)
   166  	require.Equal(t, ErrSubscriptionClosed, err)
   167  	require.Equal(t, structs.Events{}, out)
   168  }
   169  
   170  type fakeACLDelegate struct {
   171  	tokenProvider ACLTokenProvider
   172  }
   173  
   174  func (d *fakeACLDelegate) TokenProvider() ACLTokenProvider {
   175  	return d.tokenProvider
   176  }
   177  
   178  type fakeACLTokenProvider struct {
   179  	policy    *structs.ACLPolicy
   180  	policyErr error
   181  	token     *structs.ACLToken
   182  	tokenErr  error
   183  	role      *structs.ACLRole
   184  	roleErr   error
   185  }
   186  
   187  func (p *fakeACLTokenProvider) ACLTokenBySecretID(_ memdb.WatchSet, _ string) (*structs.ACLToken, error) {
   188  	return p.token, p.tokenErr
   189  }
   190  
   191  func (p *fakeACLTokenProvider) ACLPolicyByName(_ memdb.WatchSet, _ string) (*structs.ACLPolicy, error) {
   192  	return p.policy, p.policyErr
   193  }
   194  
   195  func (p *fakeACLTokenProvider) GetACLRoleByID(_ memdb.WatchSet, _ string) (*structs.ACLRole, error) {
   196  	return p.role, p.roleErr
   197  }
   198  
   199  func TestEventBroker_handleACLUpdates_policyUpdated(t *testing.T) {
   200  	ci.Parallel(t)
   201  
   202  	ctx, cancel := context.WithCancel(context.Background())
   203  	t.Cleanup(cancel)
   204  
   205  	secretID := "some-secret-id"
   206  	cases := []struct {
   207  		policyBeforeRules string
   208  		policyAfterRules  string
   209  		topics            map[structs.Topic][]string
   210  		desc              string
   211  		event             structs.Event
   212  		policyEvent       structs.Event
   213  		shouldUnsubscribe bool
   214  		initialSubErr     bool
   215  	}{
   216  		{
   217  			desc:              "subscribed to deployments and removed access",
   218  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   219  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{}),
   220  			shouldUnsubscribe: true,
   221  			event: structs.Event{
   222  				Topic: structs.TopicDeployment,
   223  				Type:  structs.TypeDeploymentUpdate,
   224  				Payload: structs.DeploymentEvent{
   225  					Deployment: &structs.Deployment{
   226  						ID: "some-id",
   227  					},
   228  				},
   229  			},
   230  			policyEvent: structs.Event{
   231  				Topic:   structs.TopicACLToken,
   232  				Type:    structs.TypeACLTokenUpserted,
   233  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   234  			},
   235  		},
   236  		{
   237  			desc:              "subscribed to evals and removed access",
   238  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   239  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{}),
   240  			shouldUnsubscribe: true,
   241  			event: structs.Event{
   242  				Topic: structs.TopicEvaluation,
   243  				Type:  structs.TypeEvalUpdated,
   244  				Payload: structs.EvaluationEvent{
   245  					Evaluation: &structs.Evaluation{
   246  						ID: "some-id",
   247  					},
   248  				},
   249  			},
   250  			policyEvent: structs.Event{
   251  				Topic:   structs.TopicACLToken,
   252  				Type:    structs.TypeACLTokenUpserted,
   253  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   254  			},
   255  		},
   256  		{
   257  			desc:              "subscribed to allocs and removed access",
   258  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   259  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{}),
   260  			shouldUnsubscribe: true,
   261  			event: structs.Event{
   262  				Topic: structs.TopicAllocation,
   263  				Type:  structs.TypeAllocationUpdated,
   264  				Payload: structs.AllocationEvent{
   265  					Allocation: &structs.Allocation{
   266  						ID: "some-id",
   267  					},
   268  				},
   269  			},
   270  			policyEvent: structs.Event{
   271  				Topic:   structs.TopicACLToken,
   272  				Type:    structs.TypeACLTokenUpserted,
   273  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   274  			},
   275  		},
   276  		{
   277  			desc:              "subscribed to nodes and removed access",
   278  			policyBeforeRules: mock.NodePolicy(acl.PolicyRead),
   279  			policyAfterRules:  mock.NodePolicy(acl.PolicyDeny),
   280  			shouldUnsubscribe: true,
   281  			event: structs.Event{
   282  				Topic: structs.TopicNode,
   283  				Type:  structs.TypeNodeRegistration,
   284  				Payload: structs.NodeStreamEvent{
   285  					Node: &structs.Node{
   286  						ID: "some-id",
   287  					},
   288  				},
   289  			},
   290  			policyEvent: structs.Event{
   291  				Topic:   structs.TopicACLToken,
   292  				Type:    structs.TypeACLTokenUpserted,
   293  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   294  			},
   295  		},
   296  		{
   297  			desc:              "subscribed to evals in all namespaces and removed access",
   298  			policyBeforeRules: mock.NamespacePolicy("*", "", []string{acl.NamespaceCapabilityReadJob}),
   299  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   300  			shouldUnsubscribe: true,
   301  			event: structs.Event{
   302  				Topic:     structs.TopicEvaluation,
   303  				Type:      structs.TypeEvalUpdated,
   304  				Namespace: "foo",
   305  				Payload: structs.EvaluationEvent{
   306  					Evaluation: &structs.Evaluation{
   307  						ID: "some-id",
   308  					},
   309  				},
   310  			},
   311  			policyEvent: structs.Event{
   312  				Topic:   structs.TopicACLToken,
   313  				Type:    structs.TypeACLTokenUpserted,
   314  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   315  			},
   316  		},
   317  		{
   318  			desc:              "subscribed to deployments and no access change",
   319  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   320  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   321  			shouldUnsubscribe: false,
   322  			event: structs.Event{
   323  				Topic: structs.TopicDeployment,
   324  				Type:  structs.TypeDeploymentUpdate,
   325  				Payload: structs.DeploymentEvent{
   326  					Deployment: &structs.Deployment{
   327  						ID: "some-id",
   328  					},
   329  				},
   330  			},
   331  			policyEvent: structs.Event{
   332  				Topic:   structs.TopicACLToken,
   333  				Type:    structs.TypeACLTokenUpserted,
   334  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   335  			},
   336  		},
   337  		{
   338  			desc:              "subscribed to evals and no access change",
   339  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   340  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   341  			shouldUnsubscribe: false,
   342  			event: structs.Event{
   343  				Topic: structs.TopicEvaluation,
   344  				Type:  structs.TypeEvalUpdated,
   345  				Payload: structs.EvaluationEvent{
   346  					Evaluation: &structs.Evaluation{
   347  						ID: "some-id",
   348  					},
   349  				},
   350  			},
   351  			policyEvent: structs.Event{
   352  				Topic:   structs.TopicACLToken,
   353  				Type:    structs.TypeACLTokenUpserted,
   354  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   355  			},
   356  		},
   357  		{
   358  			desc:              "subscribed to allocs and no access change",
   359  			policyBeforeRules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   360  			policyAfterRules:  mock.NamespacePolicy(structs.DefaultNamespace, "", []string{acl.NamespaceCapabilityReadJob}),
   361  			shouldUnsubscribe: false,
   362  			event: structs.Event{
   363  				Topic: structs.TopicAllocation,
   364  				Type:  structs.TypeAllocationUpdated,
   365  				Payload: structs.AllocationEvent{
   366  					Allocation: &structs.Allocation{
   367  						ID: "some-id",
   368  					},
   369  				},
   370  			},
   371  			policyEvent: structs.Event{
   372  				Topic:   structs.TopicACLToken,
   373  				Type:    structs.TypeACLTokenUpserted,
   374  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   375  			},
   376  		},
   377  		{
   378  			desc:              "subscribed to nodes and no access change",
   379  			policyBeforeRules: mock.NodePolicy(acl.PolicyRead),
   380  			policyAfterRules:  mock.NodePolicy(acl.PolicyRead),
   381  			shouldUnsubscribe: false,
   382  			event: structs.Event{
   383  				Topic: structs.TopicNode,
   384  				Type:  structs.TypeNodeRegistration,
   385  				Payload: structs.NodeStreamEvent{
   386  					Node: &structs.Node{
   387  						ID: "some-id",
   388  					},
   389  				},
   390  			},
   391  			policyEvent: structs.Event{
   392  				Topic:   structs.TopicACLToken,
   393  				Type:    structs.TypeACLTokenUpserted,
   394  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   395  			},
   396  		},
   397  		{
   398  			desc:              "initial token insufficient privileges",
   399  			initialSubErr:     true,
   400  			policyBeforeRules: mock.NodePolicy(acl.PolicyDeny),
   401  			event: structs.Event{
   402  				Topic: structs.TopicNode,
   403  				Type:  structs.TypeNodeRegistration,
   404  				Payload: structs.NodeStreamEvent{
   405  					Node: &structs.Node{
   406  						ID: "some-id",
   407  					},
   408  				},
   409  			},
   410  			policyEvent: structs.Event{
   411  				Topic:   structs.TopicACLToken,
   412  				Type:    structs.TypeACLTokenUpserted,
   413  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: secretID}),
   414  			},
   415  		},
   416  		{
   417  			desc:              "subscribed to nodes and policy change no change",
   418  			policyBeforeRules: mock.NodePolicy(acl.PolicyRead),
   419  			policyAfterRules:  mock.NodePolicy(acl.PolicyWrite),
   420  			shouldUnsubscribe: false,
   421  			event: structs.Event{
   422  				Topic: structs.TopicNode,
   423  				Type:  structs.TypeNodeRegistration,
   424  				Payload: structs.NodeStreamEvent{
   425  					Node: &structs.Node{
   426  						ID: "some-id",
   427  					},
   428  				},
   429  			},
   430  			policyEvent: structs.Event{
   431  				Topic: structs.TopicACLPolicy,
   432  				Type:  structs.TypeACLPolicyUpserted,
   433  				Payload: &structs.ACLPolicyEvent{
   434  					ACLPolicy: &structs.ACLPolicy{
   435  						Name: "some-policy",
   436  					},
   437  				},
   438  			},
   439  		},
   440  		{
   441  			desc:              "subscribed to nodes and policy change no access",
   442  			policyBeforeRules: mock.NodePolicy(acl.PolicyRead),
   443  			policyAfterRules:  mock.NodePolicy(acl.PolicyDeny),
   444  			shouldUnsubscribe: true,
   445  			event: structs.Event{
   446  				Topic: structs.TopicNode,
   447  				Type:  structs.TypeNodeRegistration,
   448  				Payload: structs.NodeStreamEvent{
   449  					Node: &structs.Node{
   450  						ID: "some-id",
   451  					},
   452  				},
   453  			},
   454  			policyEvent: structs.Event{
   455  				Topic: structs.TopicACLPolicy,
   456  				Type:  structs.TypeACLPolicyUpserted,
   457  				Payload: &structs.ACLPolicyEvent{
   458  					ACLPolicy: &structs.ACLPolicy{
   459  						Name: "some-policy",
   460  					},
   461  				},
   462  			},
   463  		},
   464  		{
   465  			desc:              "subscribed to nodes policy deleted",
   466  			policyBeforeRules: mock.NodePolicy(acl.PolicyRead),
   467  			policyAfterRules:  "",
   468  			shouldUnsubscribe: true,
   469  			event: structs.Event{
   470  				Topic: structs.TopicNode,
   471  				Type:  structs.TypeNodeRegistration,
   472  				Payload: structs.NodeStreamEvent{
   473  					Node: &structs.Node{
   474  						ID: "some-id",
   475  					},
   476  				},
   477  			},
   478  			policyEvent: structs.Event{
   479  				Topic: structs.TopicACLPolicy,
   480  				Type:  structs.TypeACLPolicyDeleted,
   481  				Payload: &structs.ACLPolicyEvent{
   482  					ACLPolicy: &structs.ACLPolicy{
   483  						Name: "some-policy",
   484  					},
   485  				},
   486  			},
   487  		},
   488  	}
   489  
   490  	for _, tc := range cases {
   491  		t.Run(tc.desc, func(t *testing.T) {
   492  
   493  			policy := &structs.ACLPolicy{
   494  				Name:  "some-policy",
   495  				Rules: tc.policyBeforeRules,
   496  			}
   497  			policy.SetHash()
   498  
   499  			tokenProvider := &fakeACLTokenProvider{
   500  				policy: policy,
   501  				token: &structs.ACLToken{
   502  					SecretID: secretID,
   503  					Policies: []string{policy.Name},
   504  				},
   505  			}
   506  
   507  			aclDelegate := &fakeACLDelegate{
   508  				tokenProvider: tokenProvider,
   509  			}
   510  
   511  			publisher, err := NewEventBroker(ctx, aclDelegate, EventBrokerCfg{})
   512  			require.NoError(t, err)
   513  
   514  			var ns string
   515  			if tc.event.Namespace != "" {
   516  				ns = tc.event.Namespace
   517  			} else {
   518  				ns = structs.DefaultNamespace
   519  			}
   520  
   521  			sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
   522  				Topics: map[structs.Topic][]string{
   523  					tc.event.Topic: {"*"},
   524  				},
   525  				Namespace: ns,
   526  				Token:     secretID,
   527  			})
   528  			require.Nil(t, expiryTime)
   529  
   530  			if tc.initialSubErr {
   531  				require.Error(t, err)
   532  				require.Nil(t, sub)
   533  				return
   534  			} else {
   535  				require.NoError(t, err)
   536  			}
   537  			publisher.Publish(&structs.Events{Index: 100, Events: []structs.Event{tc.event}})
   538  
   539  			ctx, cancel := context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   540  			defer cancel()
   541  			_, err = sub.Next(ctx)
   542  			require.NoError(t, err)
   543  
   544  			// Update the mock provider to use the after rules
   545  			policyAfter := &structs.ACLPolicy{
   546  				Name:        "some-new-policy",
   547  				Rules:       tc.policyAfterRules,
   548  				ModifyIndex: 101, // The ModifyIndex is used to caclulate the acl cache key
   549  			}
   550  			policyAfter.SetHash()
   551  
   552  			tokenProvider.policy = policyAfter
   553  
   554  			// Publish ACL event triggering subscription re-evaluation
   555  			publisher.Publish(&structs.Events{Index: 101, Events: []structs.Event{tc.policyEvent}})
   556  			// Publish another event
   557  			publisher.Publish(&structs.Events{Index: 102, Events: []structs.Event{tc.event}})
   558  
   559  			// If we are expecting to unsubscribe consume the subscription
   560  			// until the expected error occurs.
   561  			ctx, cancel = context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   562  			defer cancel()
   563  			if tc.shouldUnsubscribe {
   564  				for {
   565  					_, err = sub.Next(ctx)
   566  					if err != nil {
   567  						if err == context.DeadlineExceeded {
   568  							require.Fail(t, err.Error())
   569  						}
   570  						if err == ErrSubscriptionClosed {
   571  							break
   572  						}
   573  					}
   574  				}
   575  			} else {
   576  				_, err = sub.Next(ctx)
   577  				require.NoError(t, err)
   578  			}
   579  
   580  			publisher.Publish(&structs.Events{Index: 103, Events: []structs.Event{tc.event}})
   581  
   582  			ctx, cancel = context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   583  			defer cancel()
   584  			_, err = sub.Next(ctx)
   585  			if tc.shouldUnsubscribe {
   586  				require.Equal(t, ErrSubscriptionClosed, err)
   587  			} else {
   588  				require.NoError(t, err)
   589  			}
   590  		})
   591  	}
   592  }
   593  
   594  func TestEventBroker_handleACLUpdates_roleUpdated(t *testing.T) {
   595  	ci.Parallel(t)
   596  
   597  	ctx, cancel := context.WithCancel(context.Background())
   598  	t.Cleanup(cancel)
   599  
   600  	// Generate a UUID to use in all tests for the token secret ID and the role
   601  	// ID.
   602  	tokenSecretID := uuid.Generate()
   603  	roleID := uuid.Generate()
   604  
   605  	cases := []struct {
   606  		name                  string
   607  		aclPolicy             *structs.ACLPolicy
   608  		roleBeforePolicyLinks []*structs.ACLRolePolicyLink
   609  		roleAfterPolicyLinks  []*structs.ACLRolePolicyLink
   610  		topics                map[structs.Topic][]string
   611  		event                 structs.Event
   612  		policyEvent           structs.Event
   613  		shouldUnsubscribe     bool
   614  		initialSubErr         bool
   615  	}{
   616  		{
   617  			name: "deployments access policy link removed",
   618  			aclPolicy: &structs.ACLPolicy{
   619  				Name: "test-event-broker-acl-policy",
   620  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   621  					acl.NamespaceCapabilityReadJob},
   622  				),
   623  			},
   624  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   625  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{},
   626  			shouldUnsubscribe:     true,
   627  			event: structs.Event{
   628  				Topic:   structs.TopicDeployment,
   629  				Type:    structs.TypeDeploymentUpdate,
   630  				Payload: structs.DeploymentEvent{Deployment: &structs.Deployment{}},
   631  			},
   632  			policyEvent: structs.Event{
   633  				Topic:   structs.TopicACLToken,
   634  				Type:    structs.TypeACLTokenUpserted,
   635  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   636  			},
   637  		},
   638  		{
   639  			name: "evaluations access policy link removed",
   640  			aclPolicy: &structs.ACLPolicy{
   641  				Name: "test-event-broker-acl-policy",
   642  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   643  					acl.NamespaceCapabilityReadJob},
   644  				),
   645  			},
   646  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   647  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{},
   648  			shouldUnsubscribe:     true,
   649  			event: structs.Event{
   650  				Topic:   structs.TopicEvaluation,
   651  				Type:    structs.TypeEvalUpdated,
   652  				Payload: structs.EvaluationEvent{Evaluation: &structs.Evaluation{}},
   653  			},
   654  			policyEvent: structs.Event{
   655  				Topic:   structs.TopicACLToken,
   656  				Type:    structs.TypeACLTokenUpserted,
   657  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   658  			},
   659  		},
   660  		{
   661  			name: "allocations access policy link removed",
   662  			aclPolicy: &structs.ACLPolicy{
   663  				Name: "test-event-broker-acl-policy",
   664  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   665  					acl.NamespaceCapabilityReadJob},
   666  				),
   667  			},
   668  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   669  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{},
   670  			shouldUnsubscribe:     true,
   671  			event: structs.Event{
   672  				Topic:   structs.TopicAllocation,
   673  				Type:    structs.TypeAllocationUpdated,
   674  				Payload: structs.AllocationEvent{Allocation: &structs.Allocation{}},
   675  			},
   676  			policyEvent: structs.Event{
   677  				Topic:   structs.TopicACLToken,
   678  				Type:    structs.TypeACLTokenUpserted,
   679  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   680  			},
   681  		},
   682  		{
   683  			name: "nodes access policy link removed",
   684  			aclPolicy: &structs.ACLPolicy{
   685  				Name:  "test-event-broker-acl-policy",
   686  				Rules: mock.NodePolicy(acl.PolicyRead),
   687  			},
   688  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   689  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{},
   690  			shouldUnsubscribe:     true,
   691  			event: structs.Event{
   692  				Topic:   structs.TopicNode,
   693  				Type:    structs.TypeNodeRegistration,
   694  				Payload: structs.NodeStreamEvent{Node: &structs.Node{}},
   695  			},
   696  			policyEvent: structs.Event{
   697  				Topic:   structs.TopicACLToken,
   698  				Type:    structs.TypeACLTokenUpserted,
   699  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   700  			},
   701  		},
   702  		{
   703  			name: "deployment access no change",
   704  			aclPolicy: &structs.ACLPolicy{
   705  				Name: "test-event-broker-acl-policy",
   706  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   707  					acl.NamespaceCapabilityReadJob},
   708  				),
   709  			},
   710  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   711  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   712  			shouldUnsubscribe:     false,
   713  			event: structs.Event{
   714  				Topic:   structs.TopicDeployment,
   715  				Type:    structs.TypeDeploymentUpdate,
   716  				Payload: structs.DeploymentEvent{Deployment: &structs.Deployment{}},
   717  			},
   718  			policyEvent: structs.Event{
   719  				Topic:   structs.TopicACLToken,
   720  				Type:    structs.TypeACLTokenUpserted,
   721  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   722  			},
   723  		},
   724  		{
   725  			name: "evaluations access no change",
   726  			aclPolicy: &structs.ACLPolicy{
   727  				Name: "test-event-broker-acl-policy",
   728  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   729  					acl.NamespaceCapabilityReadJob},
   730  				),
   731  			},
   732  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   733  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   734  			shouldUnsubscribe:     false,
   735  			event: structs.Event{
   736  				Topic:   structs.TopicEvaluation,
   737  				Type:    structs.TypeEvalUpdated,
   738  				Payload: structs.EvaluationEvent{Evaluation: &structs.Evaluation{}},
   739  			},
   740  			policyEvent: structs.Event{
   741  				Topic:   structs.TopicACLToken,
   742  				Type:    structs.TypeACLTokenUpserted,
   743  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   744  			},
   745  		},
   746  		{
   747  			name: "allocations access no change",
   748  			aclPolicy: &structs.ACLPolicy{
   749  				Name: "test-event-broker-acl-policy",
   750  				Rules: mock.NamespacePolicy(structs.DefaultNamespace, "", []string{
   751  					acl.NamespaceCapabilityReadJob},
   752  				),
   753  			},
   754  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   755  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   756  			shouldUnsubscribe:     false,
   757  			event: structs.Event{
   758  				Topic:   structs.TopicAllocation,
   759  				Type:    structs.TypeAllocationUpdated,
   760  				Payload: structs.AllocationEvent{Allocation: &structs.Allocation{}},
   761  			},
   762  			policyEvent: structs.Event{
   763  				Topic:   structs.TopicACLToken,
   764  				Type:    structs.TypeACLTokenUpserted,
   765  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   766  			},
   767  		},
   768  		{
   769  			name: "nodes access no change",
   770  			aclPolicy: &structs.ACLPolicy{
   771  				Name:  "test-event-broker-acl-policy",
   772  				Rules: mock.NodePolicy(acl.PolicyRead),
   773  			},
   774  			roleBeforePolicyLinks: []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   775  			roleAfterPolicyLinks:  []*structs.ACLRolePolicyLink{{Name: "test-event-broker-acl-policy"}},
   776  			shouldUnsubscribe:     false,
   777  			event: structs.Event{
   778  				Topic:   structs.TopicNode,
   779  				Type:    structs.TypeNodeRegistration,
   780  				Payload: structs.NodeStreamEvent{Node: &structs.Node{}},
   781  			},
   782  			policyEvent: structs.Event{
   783  				Topic:   structs.TopicACLToken,
   784  				Type:    structs.TypeACLTokenUpserted,
   785  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tokenSecretID}),
   786  			},
   787  		},
   788  	}
   789  
   790  	for _, tc := range cases {
   791  		t.Run(tc.name, func(t *testing.T) {
   792  
   793  			// Build our fake token provider containing the relevant state
   794  			// objects and add this to our new delegate. Keeping the token
   795  			// provider setup separate means we can easily update its state.
   796  			tokenProvider := &fakeACLTokenProvider{
   797  				policy: tc.aclPolicy,
   798  				token: &structs.ACLToken{
   799  					SecretID: tokenSecretID,
   800  					Roles:    []*structs.ACLTokenRoleLink{{ID: roleID}},
   801  				},
   802  				role: &structs.ACLRole{
   803  					ID: uuid.Short(),
   804  					Policies: []*structs.ACLRolePolicyLink{
   805  						{Name: tc.aclPolicy.Name},
   806  					},
   807  				},
   808  			}
   809  			aclDelegate := &fakeACLDelegate{tokenProvider: tokenProvider}
   810  
   811  			publisher, err := NewEventBroker(ctx, aclDelegate, EventBrokerCfg{})
   812  			require.NoError(t, err)
   813  
   814  			ns := structs.DefaultNamespace
   815  			if tc.event.Namespace != "" {
   816  				ns = tc.event.Namespace
   817  			}
   818  
   819  			sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
   820  				Topics:    map[structs.Topic][]string{tc.event.Topic: {"*"}},
   821  				Namespace: ns,
   822  				Token:     tokenSecretID,
   823  			})
   824  			require.Nil(t, expiryTime)
   825  
   826  			if tc.initialSubErr {
   827  				require.Error(t, err)
   828  				require.Nil(t, sub)
   829  				return
   830  			}
   831  
   832  			require.NoError(t, err)
   833  			publisher.Publish(&structs.Events{Index: 100, Events: []structs.Event{tc.event}})
   834  
   835  			ctx, cancel := context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   836  			defer cancel()
   837  			_, err = sub.Next(ctx)
   838  			require.NoError(t, err)
   839  
   840  			// Overwrite the ACL role policy links with the updated version
   841  			// which is expected to cause a change in the subscription.
   842  			tokenProvider.role.Policies = tc.roleAfterPolicyLinks
   843  
   844  			// Publish ACL event triggering subscription re-evaluation
   845  			publisher.Publish(&structs.Events{Index: 101, Events: []structs.Event{tc.policyEvent}})
   846  			publisher.Publish(&structs.Events{Index: 102, Events: []structs.Event{tc.event}})
   847  
   848  			// If we are expecting to unsubscribe consume the subscription
   849  			// until the expected error occurs.
   850  			ctx, cancel = context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   851  			defer cancel()
   852  			if tc.shouldUnsubscribe {
   853  				for {
   854  					_, err = sub.Next(ctx)
   855  					if err != nil {
   856  						if err == context.DeadlineExceeded {
   857  							require.Fail(t, err.Error())
   858  						}
   859  						if err == ErrSubscriptionClosed {
   860  							break
   861  						}
   862  					}
   863  				}
   864  			} else {
   865  				_, err = sub.Next(ctx)
   866  				require.NoError(t, err)
   867  			}
   868  
   869  			publisher.Publish(&structs.Events{Index: 103, Events: []structs.Event{tc.event}})
   870  
   871  			ctx, cancel = context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   872  			defer cancel()
   873  			_, err = sub.Next(ctx)
   874  			if tc.shouldUnsubscribe {
   875  				require.Equal(t, ErrSubscriptionClosed, err)
   876  			} else {
   877  				require.NoError(t, err)
   878  			}
   879  		})
   880  	}
   881  }
   882  
   883  func TestEventBroker_handleACLUpdates_tokenExpiry(t *testing.T) {
   884  	ci.Parallel(t)
   885  
   886  	ctx, cancel := context.WithCancel(context.Background())
   887  	t.Cleanup(cancel)
   888  
   889  	cases := []struct {
   890  		name         string
   891  		inputToken   *structs.ACLToken
   892  		shouldExpire bool
   893  	}{
   894  		{
   895  			name: "token does not expire",
   896  			inputToken: &structs.ACLToken{
   897  				AccessorID:     uuid.Generate(),
   898  				SecretID:       uuid.Generate(),
   899  				ExpirationTime: pointer.Of(time.Now().Add(100000 * time.Hour).UTC()),
   900  				Type:           structs.ACLManagementToken,
   901  			},
   902  			shouldExpire: false,
   903  		},
   904  		{
   905  			name: "token does expire",
   906  			inputToken: &structs.ACLToken{
   907  				AccessorID:     uuid.Generate(),
   908  				SecretID:       uuid.Generate(),
   909  				ExpirationTime: pointer.Of(time.Now().Add(100000 * time.Hour).UTC()),
   910  				Type:           structs.ACLManagementToken,
   911  			},
   912  			shouldExpire: true,
   913  		},
   914  	}
   915  
   916  	for _, tc := range cases {
   917  		t.Run(tc.name, func(t *testing.T) {
   918  
   919  			// Build our fake token provider containing the relevant state
   920  			// objects and add this to our new delegate. Keeping the token
   921  			// provider setup separate means we can easily update its state.
   922  			tokenProvider := &fakeACLTokenProvider{token: tc.inputToken}
   923  			aclDelegate := &fakeACLDelegate{tokenProvider: tokenProvider}
   924  
   925  			publisher, err := NewEventBroker(ctx, aclDelegate, EventBrokerCfg{})
   926  			require.NoError(t, err)
   927  
   928  			fakeNodeEvent := structs.Event{
   929  				Topic:   structs.TopicNode,
   930  				Type:    structs.TypeNodeRegistration,
   931  				Payload: structs.NodeStreamEvent{Node: &structs.Node{}},
   932  			}
   933  
   934  			fakeTokenEvent := structs.Event{
   935  				Topic:   structs.TopicACLToken,
   936  				Type:    structs.TypeACLTokenUpserted,
   937  				Payload: structs.NewACLTokenEvent(&structs.ACLToken{SecretID: tc.inputToken.SecretID}),
   938  			}
   939  
   940  			sub, expiryTime, err := publisher.SubscribeWithACLCheck(&SubscribeRequest{
   941  				Topics: map[structs.Topic][]string{structs.TopicAll: {"*"}},
   942  				Token:  tc.inputToken.SecretID,
   943  			})
   944  			require.NoError(t, err)
   945  			require.NotNil(t, sub)
   946  			require.NotNil(t, expiryTime)
   947  
   948  			// Publish an event and check that there is a new item in the
   949  			// subscription queue.
   950  			publisher.Publish(&structs.Events{Index: 100, Events: []structs.Event{fakeNodeEvent}})
   951  
   952  			ctx, cancel := context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   953  			defer cancel()
   954  			_, err = sub.Next(ctx)
   955  			require.NoError(t, err)
   956  
   957  			// If the test states the token should expire, set the expiration
   958  			// time to a previous time.
   959  			if tc.shouldExpire {
   960  				tokenProvider.token.ExpirationTime = pointer.Of(
   961  					time.Date(1987, time.April, 13, 8, 3, 0, 0, time.UTC),
   962  				)
   963  			}
   964  
   965  			// Publish some events to trigger re-evaluation of the subscription.
   966  			publisher.Publish(&structs.Events{Index: 101, Events: []structs.Event{fakeTokenEvent}})
   967  			publisher.Publish(&structs.Events{Index: 102, Events: []structs.Event{fakeNodeEvent}})
   968  
   969  			// If we are expecting to unsubscribe consume the subscription
   970  			// until the expected error occurs.
   971  			ctx, cancel = context.WithDeadline(ctx, time.Now().Add(100*time.Millisecond))
   972  			defer cancel()
   973  
   974  			if tc.shouldExpire {
   975  				for {
   976  					if _, err = sub.Next(ctx); err != nil {
   977  						if err == context.DeadlineExceeded {
   978  							require.Fail(t, err.Error())
   979  						}
   980  						if err == ErrSubscriptionClosed {
   981  							break
   982  						}
   983  					}
   984  				}
   985  			} else {
   986  				_, err = sub.Next(ctx)
   987  				require.NoError(t, err)
   988  			}
   989  		})
   990  	}
   991  }
   992  
   993  func TestEventBroker_NodePool_ACL(t *testing.T) {
   994  	ci.Parallel(t)
   995  
   996  	ctx, cancel := context.WithCancel(context.Background())
   997  	t.Cleanup(cancel)
   998  
   999  	testCases := []struct {
  1000  		name        string
  1001  		token       *structs.ACLToken
  1002  		policy      *structs.ACLPolicy
  1003  		expectedErr string
  1004  	}{
  1005  		{
  1006  			name: "management token",
  1007  			token: &structs.ACLToken{
  1008  				AccessorID: uuid.Generate(),
  1009  				SecretID:   uuid.Generate(),
  1010  				Type:       structs.ACLManagementToken,
  1011  			},
  1012  		},
  1013  		{
  1014  			name: "client token",
  1015  			token: &structs.ACLToken{
  1016  				AccessorID: uuid.Generate(),
  1017  				SecretID:   uuid.Generate(),
  1018  				Type:       structs.ACLClientToken,
  1019  			},
  1020  			expectedErr: structs.ErrPermissionDenied.Error(),
  1021  		},
  1022  		{
  1023  			name: "node pool read",
  1024  			token: &structs.ACLToken{
  1025  				AccessorID: uuid.Generate(),
  1026  				SecretID:   uuid.Generate(),
  1027  				Type:       structs.ACLClientToken,
  1028  				Policies:   []string{"node-pool-read"},
  1029  			},
  1030  			policy: &structs.ACLPolicy{
  1031  				Name:  "node-pool-read",
  1032  				Rules: `node_pool "*" { policy = "read" }`,
  1033  			},
  1034  			expectedErr: structs.ErrPermissionDenied.Error(),
  1035  		},
  1036  		{
  1037  			name: "node pool write",
  1038  			token: &structs.ACLToken{
  1039  				AccessorID: uuid.Generate(),
  1040  				SecretID:   uuid.Generate(),
  1041  				Type:       structs.ACLClientToken,
  1042  				Policies:   []string{"node-pool-write"},
  1043  			},
  1044  			policy: &structs.ACLPolicy{
  1045  				Name:  "node-pool-write",
  1046  				Rules: `node_pool "*" { policy = "write" }`,
  1047  			},
  1048  			expectedErr: structs.ErrPermissionDenied.Error(),
  1049  		},
  1050  	}
  1051  
  1052  	for _, tc := range testCases {
  1053  		t.Run(tc.name, func(t *testing.T) {
  1054  			tokenProvider := &fakeACLTokenProvider{token: tc.token, policy: tc.policy}
  1055  			aclDelegate := &fakeACLDelegate{tokenProvider: tokenProvider}
  1056  
  1057  			publisher, err := NewEventBroker(ctx, aclDelegate, EventBrokerCfg{})
  1058  			must.NoError(t, err)
  1059  
  1060  			_, _, err = publisher.SubscribeWithACLCheck(&SubscribeRequest{
  1061  				Topics: map[structs.Topic][]string{structs.TopicNodePool: {"*"}},
  1062  				Token:  tc.token.SecretID,
  1063  			})
  1064  
  1065  			if tc.expectedErr != "" {
  1066  				must.ErrorContains(t, err, tc.expectedErr)
  1067  			} else {
  1068  				must.NoError(t, err)
  1069  			}
  1070  		})
  1071  	}
  1072  
  1073  }
  1074  
  1075  func consumeSubscription(ctx context.Context, sub *Subscription) <-chan subNextResult {
  1076  	eventCh := make(chan subNextResult, 1)
  1077  	go func() {
  1078  		for {
  1079  			es, err := sub.Next(ctx)
  1080  			eventCh <- subNextResult{
  1081  				Events: es.Events,
  1082  				Err:    err,
  1083  			}
  1084  			if err != nil {
  1085  				return
  1086  			}
  1087  		}
  1088  	}()
  1089  	return eventCh
  1090  }
  1091  
  1092  type subNextResult struct {
  1093  	Events []structs.Event
  1094  	Err    error
  1095  }
  1096  
  1097  func nextResult(t *testing.T, eventCh <-chan subNextResult) subNextResult {
  1098  	t.Helper()
  1099  	select {
  1100  	case next := <-eventCh:
  1101  		return next
  1102  	case <-time.After(100 * time.Millisecond):
  1103  		t.Fatalf("no event after 100ms")
  1104  	}
  1105  	return subNextResult{}
  1106  }
  1107  
  1108  func assertNoResult(t *testing.T, eventCh <-chan subNextResult) {
  1109  	t.Helper()
  1110  	select {
  1111  	case next := <-eventCh:
  1112  		require.NoError(t, next.Err)
  1113  		require.Len(t, next.Events, 1)
  1114  		t.Fatalf("received unexpected event: %#v", next.Events[0].Payload)
  1115  	case <-time.After(100 * time.Millisecond):
  1116  	}
  1117  }
  1118  
  1119  func consumeSub(ctx context.Context, sub *Subscription) error {
  1120  	for {
  1121  		_, err := sub.Next(ctx)
  1122  		if err != nil {
  1123  			return err
  1124  		}
  1125  	}
  1126  }