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 }