// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package state import ( "errors" "fmt" "github.com/hashicorp/go-memdb" "github.com/hashicorp/nomad/nomad/structs" ) // UpsertACLBindingRules is used to insert a number of ACL binding rules into // the state store. It uses a single write transaction for efficiency, however, // any error means no entries will be committed. func (s *StateStore) UpsertACLBindingRules( index uint64, bindingRules []*structs.ACLBindingRule, allowMissingAuthMethod bool) error { // Grab a write transaction. txn := s.db.WriteTxnMsgT(structs.ACLBindingRulesUpsertRequestType, index) defer txn.Abort() // updated tracks whether any inserts have been made. This allows us to // skip updating the index table if we do not need to. var updated bool // Iterate the array of rules. In the event of a single error, all inserts // fail via the txn.Abort() defer. for _, rule := range bindingRules { bindingRuleInserted, err := s.upsertACLBindingRuleTxn(index, txn, rule, allowMissingAuthMethod) if err != nil { return err } // Ensure we track whether any inserts have been made. updated = updated || bindingRuleInserted } // If we did not perform any inserts, exit early. if !updated { return nil } // Perform the index table update to mark the new insert. if err := txn.Insert(tableIndex, &IndexEntry{TableACLBindingRules, index}); err != nil { return fmt.Errorf("index update failed: %v", err) } return txn.Commit() } // upsertACLBindingRuleTxn inserts a single ACL binding rule into the state // store using the provided write transaction. It is the responsibility of the // caller to update the index table. func (s *StateStore) upsertACLBindingRuleTxn( index uint64, txn *txn, rule *structs.ACLBindingRule, allowMissingAuthMethod bool) (bool, error) { // Ensure the rule hash is not zero to provide defense in depth. This // should be done outside the state store, so we do not spend time here and // thus Raft, when it can be avoided. if len(rule.Hash) == 0 { rule.SetHash() } // This validation also happens within the RPC handler, but Raft latency // could mean that by the time the state call is invoked, another Raft // update has the auth method detailed in binding rule. Therefore, check // again while in our write txn. if !allowMissingAuthMethod { method, err := s.GetACLAuthMethodByName(nil, rule.AuthMethod) if err != nil { return false, fmt.Errorf("ACL auth method lookup failed: %v", err) } if method == nil { return false, fmt.Errorf("ACL binding rule insert failed: ACL auth method not found") } } // This validation also happens within the RPC handler, but Raft latency // could mean that by the time the state call is invoked, another Raft // update has already written a method with the same name. We therefore // need to check we are not trying to create a rule with an existing ID. existingRaw, err := txn.First(TableACLBindingRules, indexID, rule.ID) if err != nil { return false, fmt.Errorf("ACL binding rule lookup failed: %v", err) } var existing *structs.ACLBindingRule if existingRaw != nil { existing = existingRaw.(*structs.ACLBindingRule) } // Depending on whether this is an initial create, or an update, we need to // check and set certain parameters. The most important is to ensure any // create index is carried over. if existing != nil { // If the rule already exists, check whether the update contains any // difference. If it doesn't, we can avoid a state update as well as // updates to any blocking queries. if existing.Equal(rule) { return false, nil } rule.CreateIndex = existing.CreateIndex rule.ModifyIndex = index rule.CreateTime = existing.CreateTime } else { rule.CreateIndex = index rule.ModifyIndex = index } // Insert the auth method into the table. if err := txn.Insert(TableACLBindingRules, rule); err != nil { return false, fmt.Errorf("ACL binding rule insert failed: %v", err) } return true, nil } // DeleteACLBindingRules is responsible for batch deleting ACL binding rules. // It uses a single write transaction for efficiency, however, any error means // no entries will be committed. An error is produced if a rule is not found // within state which has been passed within the array. func (s *StateStore) DeleteACLBindingRules(index uint64, bindingRuleIDs []string) error { txn := s.db.WriteTxnMsgT(structs.ACLBindingRulesDeleteRequestType, index) defer txn.Abort() for _, ruleID := range bindingRuleIDs { if err := s.deleteACLBindingRuleTxn(txn, ruleID); err != nil { return err } } // Update the index table to indicate an update has occurred. if err := txn.Insert(tableIndex, &IndexEntry{TableACLBindingRules, index}); err != nil { return fmt.Errorf("index update failed: %v", err) } return txn.Commit() } // deleteACLBindingRuleTxn deletes a single ACL binding rule from the state // store using the provided write transaction. It is the responsibility of the // caller to update the index table. func (s *StateStore) deleteACLBindingRuleTxn(txn *txn, ruleID string) error { existing, err := txn.First(TableACLBindingRules, indexID, ruleID) if err != nil { return fmt.Errorf("ACL binding rule lookup failed: %v", err) } if existing == nil { return errors.New("ACL binding rule not found") } // Delete the existing entry from the table. if err := txn.Delete(TableACLBindingRules, existing); err != nil { return fmt.Errorf("ACL binding rule deletion failed: %v", err) } return nil } // GetACLBindingRules returns an iterator that contains all ACL binding rules // stored within state. func (s *StateStore) GetACLBindingRules(ws memdb.WatchSet) (memdb.ResultIterator, error) { txn := s.db.ReadTxn() // Walk the entire table to get all ACL binding rules. iter, err := txn.Get(TableACLBindingRules, indexID) if err != nil { return nil, fmt.Errorf("ACL binding rules lookup failed: %v", err) } ws.Add(iter.WatchCh()) return iter, nil } // GetACLBindingRule returns a single ACL binding rule specified by the input // ID. The binding rule object will be nil, if no matching entry was found; it // is the responsibility of the caller to check for this. func (s *StateStore) GetACLBindingRule(ws memdb.WatchSet, ruleID string) (*structs.ACLBindingRule, error) { txn := s.db.ReadTxn() // Perform the ACL binding rule lookup using the ID. watchCh, existing, err := txn.FirstWatch(TableACLBindingRules, indexID, ruleID) if err != nil { return nil, fmt.Errorf("ACL binding rule lookup failed: %v", err) } ws.Add(watchCh) if existing != nil { return existing.(*structs.ACLBindingRule), nil } return nil, nil } // GetACLBindingRulesByAuthMethod returns an iterator with all binding rules // associated with the named authentication method. func (s *StateStore) GetACLBindingRulesByAuthMethod( ws memdb.WatchSet, authMethod string) (memdb.ResultIterator, error) { txn := s.db.ReadTxn() iter, err := txn.Get(TableACLBindingRules, indexAuthMethod, authMethod) if err != nil { return nil, fmt.Errorf("ACL binding rule lookup failed: %v", err) } ws.Add(iter.WatchCh()) return iter, nil }