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