// Copyright (c) HashiCorp, Inc.
// 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
ACLDNSToken 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" `
DNS string ` json:"dns,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 )
}
if tokens . DNS != "" {
s . UpdateDNSToken ( tokens . DNS , TokenSourceAPI )
if cfg . ACLDNSToken != "" {
logger . Warn ( "\"dns\" token present in both the configuration and persisted token store, using the persisted token" )
}
} else {
s . UpdateDNSToken ( cfg . ACLDNSToken , 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
}
if tok , source := s . DNSTokenAndSource ( ) ; tok != "" && source == TokenSourceAPI {
tokens . DNS = 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
}