consul/agent/token/persistence.go

221 lines
6.5 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
package token
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/consul/lib/file"
)
// Logger used by Store.Load to report warnings.
type Logger interface {
Warn(msg string, args ...interface{})
}
// Config used by Store.Load, which includes tokens and settings for persistence.
type Config struct {
EnablePersistence bool
DataDir string
ACLDefaultToken string
ACLAgentToken string
ACLAgentRecoveryToken string
ACLReplicationToken string
ACLConfigFileRegistrationToken string
EnterpriseConfig
}
const tokensPath = "acl-tokens.json"
// Load tokens from Config and optionally from a persisted file in the cfg.DataDir.
// If a token exists in both the persisted file and in the Config a warning will
// be logged and the persisted token will be used.
//
// Failures to load the persisted file will result in loading tokens from the
// config before returning the error.
func (t *Store) Load(cfg Config, logger Logger) error {
t.persistenceLock.RLock()
if !cfg.EnablePersistence {
t.persistence = nil
t.persistenceLock.RUnlock()
loadTokens(t, cfg, persistedTokens{}, logger)
return nil
}
defer t.persistenceLock.RUnlock()
t.persistence = &fileStore{
filename: filepath.Join(cfg.DataDir, tokensPath),
logger: logger,
}
return t.persistence.load(t, cfg)
}
// WithPersistenceLock executes f while hold a lock. If f returns a nil error,
// the tokens in Store will be persisted to the tokens file. Otherwise no
// tokens will be persisted, and the error from f will be returned.
//
// The lock is held so that the writes are persisted before some other thread
// can change the value.
func (t *Store) WithPersistenceLock(f func() error) error {
t.persistenceLock.Lock()
if t.persistence == nil {
t.persistenceLock.Unlock()
return f()
}
defer t.persistenceLock.Unlock()
return t.persistence.withPersistenceLock(t, f)
}
type persistedTokens struct {
Replication string `json:"replication,omitempty"`
AgentRecovery string `json:"agent_recovery,omitempty"`
Default string `json:"default,omitempty"`
Agent string `json:"agent,omitempty"`
ConfigFileRegistration string `json:"config_file_service_registration,omitempty"`
}
type fileStore struct {
filename string
logger Logger
}
func (p *fileStore) load(s *Store, cfg Config) error {
tokens, err := readPersistedFromFile(p.filename)
if err != nil {
p.logger.Warn("unable to load persisted tokens", "error", err)
}
loadTokens(s, cfg, tokens, p.logger)
return err
}
func loadTokens(s *Store, cfg Config, tokens persistedTokens, logger Logger) {
if tokens.Default != "" {
s.UpdateUserToken(tokens.Default, TokenSourceAPI)
if cfg.ACLDefaultToken != "" {
logger.Warn("\"default\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateUserToken(cfg.ACLDefaultToken, TokenSourceConfig)
}
if tokens.Agent != "" {
s.UpdateAgentToken(tokens.Agent, TokenSourceAPI)
if cfg.ACLAgentToken != "" {
logger.Warn("\"agent\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateAgentToken(cfg.ACLAgentToken, TokenSourceConfig)
}
if tokens.AgentRecovery != "" {
s.UpdateAgentRecoveryToken(tokens.AgentRecovery, TokenSourceAPI)
if cfg.ACLAgentRecoveryToken != "" {
logger.Warn("\"agent_recovery\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateAgentRecoveryToken(cfg.ACLAgentRecoveryToken, TokenSourceConfig)
}
if tokens.Replication != "" {
s.UpdateReplicationToken(tokens.Replication, TokenSourceAPI)
if cfg.ACLReplicationToken != "" {
logger.Warn("\"replication\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateReplicationToken(cfg.ACLReplicationToken, TokenSourceConfig)
}
if tokens.ConfigFileRegistration != "" {
s.UpdateConfigFileRegistrationToken(tokens.ConfigFileRegistration, TokenSourceAPI)
if cfg.ACLConfigFileRegistrationToken != "" {
logger.Warn("\"config_file_service_registration\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateConfigFileRegistrationToken(cfg.ACLConfigFileRegistrationToken, TokenSourceConfig)
}
loadEnterpriseTokens(s, cfg)
}
func readPersistedFromFile(filename string) (persistedTokens, error) {
var tokens struct {
persistedTokens
// Support reading tokens persisted by versions <1.11, where agent_master was
// renamed to agent_recovery.
LegacyAgentMaster string `json:"agent_master"`
}
buf, err := os.ReadFile(filename)
switch {
case os.IsNotExist(err):
// non-existence is not an error we care about
return tokens.persistedTokens, nil
case err != nil:
return tokens.persistedTokens, fmt.Errorf("failed reading tokens file %q: %w", filename, err)
}
if err := json.Unmarshal(buf, &tokens); err != nil {
return tokens.persistedTokens, fmt.Errorf("failed to decode tokens file %q: %w", filename, err)
}
if tokens.AgentRecovery == "" {
tokens.AgentRecovery = tokens.LegacyAgentMaster
}
return tokens.persistedTokens, nil
}
func (p *fileStore) withPersistenceLock(s *Store, f func() error) error {
if err := f(); err != nil {
return err
}
return p.saveToFile(s)
}
func (p *fileStore) saveToFile(s *Store) error {
tokens := persistedTokens{}
if tok, source := s.UserTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Default = tok
}
if tok, source := s.AgentTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Agent = tok
}
if tok, source := s.AgentRecoveryTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.AgentRecovery = tok
}
if tok, source := s.ReplicationTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Replication = tok
}
if tok, source := s.ConfigFileRegistrationTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.ConfigFileRegistration = tok
}
data, err := json.Marshal(tokens)
if err != nil {
p.logger.Warn("failed to persist tokens", "error", err)
return fmt.Errorf("Failed to marshal tokens for persistence: %v", err)
}
if err := file.WriteAtomicWithPerms(p.filename, data, 0700, 0600); err != nil {
p.logger.Warn("failed to persist tokens", "error", err)
return fmt.Errorf("Failed to persist tokens - %v", err)
}
return nil
}