open-nomad/nomad/state/state_store_acl_binding_rule.go
James Rasell 3c941c6bc3
acl: add binding rule object state schema and functionality. (#15511)
This change adds a new table that will store ACL binding rule
objects. The two indexes allow fast lookups by their ID, or by
which auth method they are linked to. Snapshot persist and
restore functionality ensures this table can be saved and
restored from snapshots.

In order to write and delete the object to state, new Raft messages
have been added.

All RPC request and response structs, along with object functions
such as diff and canonicalize have been included within this work
as it is nicely separated from the other areas of work.
2022-12-14 08:48:18 +01:00

209 lines
7 KiB
Go

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
}