github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/nomad/stream/event_broker.go (about)

     1  package stream
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/armon/go-metrics"
    12  	"github.com/hashicorp/go-memdb"
    13  	lru "github.com/hashicorp/golang-lru"
    14  	"github.com/hashicorp/nomad/acl"
    15  	"github.com/hashicorp/nomad/nomad/structs"
    16  
    17  	"github.com/hashicorp/go-hclog"
    18  )
    19  
    20  const (
    21  	ACLCheckNodeRead   = "node-read"
    22  	ACLCheckManagement = "management"
    23  	aclCacheSize       = 32
    24  )
    25  
    26  type EventBrokerCfg struct {
    27  	EventBufferSize int64
    28  	Logger          hclog.Logger
    29  }
    30  
    31  type EventBroker struct {
    32  	// mu protects subscriptions
    33  	mu            sync.Mutex
    34  	subscriptions *subscriptions
    35  
    36  	// eventBuf stores a configurable amount of events in memory
    37  	eventBuf *eventBuffer
    38  
    39  	// publishCh is used to send messages from an active txn to a goroutine which
    40  	// publishes events, so that publishing can happen asynchronously from
    41  	// the Commit call in the FSM hot path.
    42  	publishCh chan *structs.Events
    43  
    44  	aclDelegate ACLDelegate
    45  	aclCache    *lru.TwoQueueCache
    46  
    47  	aclCh chan structs.Event
    48  
    49  	logger hclog.Logger
    50  }
    51  
    52  // NewEventBroker returns an EventBroker for publishing change events.
    53  // A goroutine is run in the background to publish events to an event buffer.
    54  // Cancelling the context will shutdown the goroutine to free resources, and stop
    55  // all publishing.
    56  func NewEventBroker(ctx context.Context, aclDelegate ACLDelegate, cfg EventBrokerCfg) (*EventBroker, error) {
    57  	if cfg.Logger == nil {
    58  		cfg.Logger = hclog.NewNullLogger()
    59  	}
    60  
    61  	// Set the event buffer size to a minimum
    62  	if cfg.EventBufferSize == 0 {
    63  		cfg.EventBufferSize = 100
    64  	}
    65  
    66  	aclCache, err := lru.New2Q(aclCacheSize)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	buffer := newEventBuffer(cfg.EventBufferSize)
    72  	e := &EventBroker{
    73  		logger:      cfg.Logger.Named("event_broker"),
    74  		eventBuf:    buffer,
    75  		publishCh:   make(chan *structs.Events, 64),
    76  		aclCh:       make(chan structs.Event, 10),
    77  		aclDelegate: aclDelegate,
    78  		aclCache:    aclCache,
    79  		subscriptions: &subscriptions{
    80  			byToken: make(map[string]map[*SubscribeRequest]*Subscription),
    81  		},
    82  	}
    83  
    84  	go e.handleUpdates(ctx)
    85  	go e.handleACLUpdates(ctx)
    86  
    87  	return e, nil
    88  }
    89  
    90  // Len returns the current length of the event buffer.
    91  func (e *EventBroker) Len() int {
    92  	return e.eventBuf.Len()
    93  }
    94  
    95  // Publish events to all subscribers of the event Topic.
    96  func (e *EventBroker) Publish(events *structs.Events) {
    97  	if len(events.Events) == 0 {
    98  		return
    99  	}
   100  
   101  	// Notify the broker to check running subscriptions against potentially
   102  	// updated ACL Token or Policy
   103  	for _, event := range events.Events {
   104  		if event.Topic == structs.TopicACLToken || event.Topic == structs.TopicACLPolicy {
   105  			e.aclCh <- event
   106  		}
   107  	}
   108  
   109  	e.publishCh <- events
   110  }
   111  
   112  // SubscribeWithACLCheck validates the SubscribeRequest's token and requested
   113  // topics to ensure that the tokens privileges are sufficient. It will also
   114  // return the token expiry time, if any. It is the callers responsibility to
   115  // check this before publishing events to the caller.
   116  func (e *EventBroker) SubscribeWithACLCheck(req *SubscribeRequest) (*Subscription, *time.Time, error) {
   117  	aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, req.Token)
   118  	if err != nil {
   119  		return nil, nil, structs.ErrPermissionDenied
   120  	}
   121  
   122  	if allowed := aclAllowsSubscription(aclObj, req); !allowed {
   123  		return nil, nil, structs.ErrPermissionDenied
   124  	}
   125  
   126  	sub, err := e.Subscribe(req)
   127  	if err != nil {
   128  		return nil, nil, err
   129  	}
   130  	return sub, expiryTime, nil
   131  }
   132  
   133  // Subscribe returns a new Subscription for a given request. A Subscription
   134  // will receive an initial empty currentItem value which points to the first item
   135  // in the buffer. This allows the new subscription to call Next() without first checking
   136  // for the current Item.
   137  //
   138  // A Subscription will start at the requested index, or as close as possible to
   139  // the requested index if it is no longer in the buffer. If StartExactlyAtIndex is
   140  // set and the index is no longer in the buffer or not yet in the buffer an error
   141  // will be returned.
   142  //
   143  // When a caller is finished with the subscription it must call Subscription.Unsubscribe
   144  // to free ACL tracking resources.
   145  func (e *EventBroker) Subscribe(req *SubscribeRequest) (*Subscription, error) {
   146  	e.mu.Lock()
   147  	defer e.mu.Unlock()
   148  
   149  	var head *bufferItem
   150  	var offset int
   151  	if req.Index != 0 {
   152  		head, offset = e.eventBuf.StartAtClosest(req.Index)
   153  	} else {
   154  		head = e.eventBuf.Head()
   155  	}
   156  	if offset > 0 && req.StartExactlyAtIndex {
   157  		return nil, fmt.Errorf("requested index not in buffer")
   158  	} else if offset > 0 {
   159  		metrics.SetGauge([]string{"nomad", "event_broker", "subscription", "request_offset"}, float32(offset))
   160  		e.logger.Debug("requested index no longer in buffer", "requsted", int(req.Index), "closest", int(head.Events.Index))
   161  	}
   162  
   163  	// Empty head so that calling Next on sub
   164  	start := newBufferItem(&structs.Events{Index: req.Index})
   165  	start.link.next.Store(head)
   166  	close(start.link.nextCh)
   167  
   168  	sub := newSubscription(req, start, e.subscriptions.unsubscribeFn(req))
   169  
   170  	e.subscriptions.add(req, sub)
   171  	return sub, nil
   172  }
   173  
   174  // CloseAll closes all subscriptions
   175  func (e *EventBroker) CloseAll() {
   176  	e.subscriptions.closeAll()
   177  }
   178  
   179  func (e *EventBroker) handleUpdates(ctx context.Context) {
   180  	for {
   181  		select {
   182  		case <-ctx.Done():
   183  			e.subscriptions.closeAll()
   184  			return
   185  		case update := <-e.publishCh:
   186  			e.eventBuf.Append(update)
   187  		}
   188  	}
   189  }
   190  
   191  func (e *EventBroker) handleACLUpdates(ctx context.Context) {
   192  	for {
   193  		select {
   194  		case <-ctx.Done():
   195  			return
   196  		case update := <-e.aclCh:
   197  			switch payload := update.Payload.(type) {
   198  			case *structs.ACLTokenEvent:
   199  				tokenSecretID := payload.SecretID()
   200  
   201  				// Token was deleted
   202  				if update.Type == structs.TypeACLTokenDeleted {
   203  					e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
   204  					continue
   205  				}
   206  
   207  				// If broker cannot fetch state there is nothing more to do
   208  				if e.aclDelegate == nil {
   209  					continue
   210  				}
   211  
   212  				aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(e.aclDelegate.TokenProvider(), e.aclCache, tokenSecretID)
   213  				if err != nil || aclObj == nil {
   214  					e.logger.Error("failed resolving ACL for secretID, closing subscriptions", "error", err)
   215  					e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
   216  					continue
   217  				}
   218  
   219  				if expiryTime != nil && expiryTime.Before(time.Now().UTC()) {
   220  					e.logger.Info("ACL token is expired, closing subscriptions")
   221  					e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
   222  					continue
   223  				}
   224  
   225  				e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool {
   226  					return !aclAllowsSubscription(aclObj, sub.req)
   227  				})
   228  
   229  			case *structs.ACLPolicyEvent, *structs.ACLRoleStreamEvent:
   230  				// Re-evaluate each subscription permission since a policy or
   231  				// role change may alter the permissions of the token being
   232  				// used for the subscription.
   233  				e.checkSubscriptionsAgainstACLChange()
   234  			}
   235  		}
   236  	}
   237  }
   238  
   239  // checkSubscriptionsAgainstACLChange iterates over the brokers subscriptions
   240  // and evaluates whether the token used for the subscription is still valid. A
   241  // token may become invalid is the assigned policies or roles have been updated
   242  // which removed the required permission. If the token is no long valid, the
   243  // subscription is closed.
   244  func (e *EventBroker) checkSubscriptionsAgainstACLChange() {
   245  	e.mu.Lock()
   246  	defer e.mu.Unlock()
   247  
   248  	// If broker cannot fetch state there is nothing more to do
   249  	if e.aclDelegate == nil {
   250  		return
   251  	}
   252  
   253  	aclSnapshot := e.aclDelegate.TokenProvider()
   254  	for tokenSecretID := range e.subscriptions.byToken {
   255  		// if tokenSecretID is empty ACLs were disabled at time of subscribing
   256  		if tokenSecretID == "" {
   257  			continue
   258  		}
   259  
   260  		aclObj, expiryTime, err := aclObjFromSnapshotForTokenSecretID(aclSnapshot, e.aclCache, tokenSecretID)
   261  		if err != nil || aclObj == nil {
   262  			e.logger.Debug("failed resolving ACL for secretID, closing subscriptions", "error", err)
   263  			e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
   264  			continue
   265  		}
   266  
   267  		if expiryTime != nil && expiryTime.Before(time.Now().UTC()) {
   268  			e.logger.Info("ACL token is expired, closing subscriptions")
   269  			e.subscriptions.closeSubscriptionsForTokens([]string{tokenSecretID})
   270  			continue
   271  		}
   272  
   273  		e.subscriptions.closeSubscriptionFunc(tokenSecretID, func(sub *Subscription) bool {
   274  			return !aclAllowsSubscription(aclObj, sub.req)
   275  		})
   276  	}
   277  }
   278  
   279  func aclObjFromSnapshotForTokenSecretID(
   280  	aclSnapshot ACLTokenProvider, aclCache *lru.TwoQueueCache, tokenSecretID string) (
   281  	*acl.ACL, *time.Time, error) {
   282  
   283  	aclToken, err := aclSnapshot.ACLTokenBySecretID(nil, tokenSecretID)
   284  	if err != nil {
   285  		return nil, nil, err
   286  	}
   287  
   288  	if aclToken == nil {
   289  		return nil, nil, structs.ErrTokenNotFound
   290  	}
   291  	if aclToken.IsExpired(time.Now().UTC()) {
   292  		return nil, nil, structs.ErrTokenExpired
   293  	}
   294  
   295  	// Check if this is a management token
   296  	if aclToken.Type == structs.ACLManagementToken {
   297  		return acl.ManagementACL, aclToken.ExpirationTime, nil
   298  	}
   299  
   300  	aclPolicies := make([]*structs.ACLPolicy, 0, len(aclToken.Policies)+len(aclToken.Roles))
   301  
   302  	for _, policyName := range aclToken.Policies {
   303  		policy, err := aclSnapshot.ACLPolicyByName(nil, policyName)
   304  		if err != nil || policy == nil {
   305  			return nil, nil, errors.New("error finding acl policy")
   306  		}
   307  		aclPolicies = append(aclPolicies, policy)
   308  	}
   309  
   310  	// Iterate all the token role links, so we can unpack these and identify
   311  	// the ACL policies.
   312  	for _, roleLink := range aclToken.Roles {
   313  
   314  		role, err := aclSnapshot.GetACLRoleByID(nil, roleLink.ID)
   315  		if err != nil {
   316  			return nil, nil, err
   317  		}
   318  		if role == nil {
   319  			continue
   320  		}
   321  
   322  		for _, policyLink := range role.Policies {
   323  			policy, err := aclSnapshot.ACLPolicyByName(nil, policyLink.Name)
   324  			if err != nil || policy == nil {
   325  				return nil, nil, errors.New("error finding acl policy")
   326  			}
   327  			aclPolicies = append(aclPolicies, policy)
   328  		}
   329  	}
   330  
   331  	aclObj, err := structs.CompileACLObject(aclCache, aclPolicies)
   332  	if err != nil {
   333  		return nil, nil, err
   334  	}
   335  	return aclObj, aclToken.ExpirationTime, nil
   336  }
   337  
   338  type ACLTokenProvider interface {
   339  	ACLTokenBySecretID(ws memdb.WatchSet, secretID string) (*structs.ACLToken, error)
   340  	ACLPolicyByName(ws memdb.WatchSet, policyName string) (*structs.ACLPolicy, error)
   341  	GetACLRoleByID(ws memdb.WatchSet, roleID string) (*structs.ACLRole, error)
   342  }
   343  
   344  type ACLDelegate interface {
   345  	TokenProvider() ACLTokenProvider
   346  }
   347  
   348  func aclAllowsSubscription(aclObj *acl.ACL, subReq *SubscribeRequest) bool {
   349  	for topic := range subReq.Topics {
   350  		switch topic {
   351  		case structs.TopicDeployment,
   352  			structs.TopicEvaluation,
   353  			structs.TopicAllocation,
   354  			structs.TopicJob,
   355  			structs.TopicService:
   356  			if ok := aclObj.AllowNsOp(subReq.Namespace, acl.NamespaceCapabilityReadJob); !ok {
   357  				return false
   358  			}
   359  		case structs.TopicNode:
   360  			if ok := aclObj.AllowNodeRead(); !ok {
   361  				return false
   362  			}
   363  		default:
   364  			if ok := aclObj.IsManagement(); !ok {
   365  				return false
   366  			}
   367  		}
   368  	}
   369  
   370  	return true
   371  }
   372  
   373  func (s *Subscription) forceClose() {
   374  	if atomic.CompareAndSwapUint32(&s.state, subscriptionStateOpen, subscriptionStateClosed) {
   375  		close(s.forceClosed)
   376  	}
   377  }
   378  
   379  type subscriptions struct {
   380  	// mu for byToken. If both subscription.mu and EventBroker.mu need
   381  	// to be held, EventBroker mutex MUST always be acquired first.
   382  	mu sync.RWMutex
   383  
   384  	// byToken is an mapping of active Subscriptions indexed by a token and
   385  	// a pointer to the request.
   386  	// When the token is modified all subscriptions under that token will be
   387  	// reloaded.
   388  	// A subscription may be unsubscribed by using the pointer to the request.
   389  	byToken map[string]map[*SubscribeRequest]*Subscription
   390  }
   391  
   392  func (s *subscriptions) add(req *SubscribeRequest, sub *Subscription) {
   393  	s.mu.Lock()
   394  	defer s.mu.Unlock()
   395  
   396  	subsByToken, ok := s.byToken[req.Token]
   397  	if !ok {
   398  		subsByToken = make(map[*SubscribeRequest]*Subscription)
   399  		s.byToken[req.Token] = subsByToken
   400  	}
   401  	subsByToken[req] = sub
   402  }
   403  
   404  func (s *subscriptions) closeSubscriptionsForTokens(tokenSecretIDs []string) {
   405  	s.mu.RLock()
   406  	defer s.mu.RUnlock()
   407  
   408  	for _, secretID := range tokenSecretIDs {
   409  		if subs, ok := s.byToken[secretID]; ok {
   410  			for _, sub := range subs {
   411  				sub.forceClose()
   412  			}
   413  		}
   414  	}
   415  }
   416  
   417  func (s *subscriptions) closeSubscriptionFunc(tokenSecretID string, fn func(*Subscription) bool) {
   418  	s.mu.RLock()
   419  	defer s.mu.RUnlock()
   420  
   421  	for _, sub := range s.byToken[tokenSecretID] {
   422  		if fn(sub) {
   423  			sub.forceClose()
   424  		}
   425  	}
   426  }
   427  
   428  // unsubscribeFn returns a function that the subscription will call to remove
   429  // itself from the subsByToken.
   430  // This function is returned as a closure so that the caller doesn't need to keep
   431  // track of the SubscriptionRequest, and can not accidentally call unsubscribeFn with the
   432  // wrong pointer.
   433  func (s *subscriptions) unsubscribeFn(req *SubscribeRequest) func() {
   434  	return func() {
   435  		s.mu.Lock()
   436  		defer s.mu.Unlock()
   437  
   438  		subsByToken, ok := s.byToken[req.Token]
   439  		if !ok {
   440  			return
   441  		}
   442  
   443  		sub := subsByToken[req]
   444  		if sub == nil {
   445  			return
   446  		}
   447  
   448  		// close the subscription
   449  		sub.forceClose()
   450  
   451  		delete(subsByToken, req)
   452  		if len(subsByToken) == 0 {
   453  			delete(s.byToken, req.Token)
   454  		}
   455  	}
   456  }
   457  
   458  func (s *subscriptions) closeAll() {
   459  	s.mu.Lock()
   460  	defer s.mu.Unlock()
   461  
   462  	for _, byRequest := range s.byToken {
   463  		for _, sub := range byRequest {
   464  			sub.forceClose()
   465  		}
   466  	}
   467  }