Module: ChefConfig::Mixin::Credentials

Included in:
TrainTransport, WorkstationConfigLoader
Defined in:
lib/chef-config/mixin/credentials.rb

Overview

Helper methods for working with credentials files.

Since:

  • 13.7

Constant Summary collapse

GLOBAL_CONFIG_HASHES =

Since:

  • 13.7

%w{ default_secrets_provider }.freeze
SUPPORTED_SECRETS_PROVIDERS =

Since:

  • 13.7

%w{ hashicorp-vault }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#credentials_configObject (readonly)

Since:

  • 13.7



29
30
31
# File 'lib/chef-config/mixin/credentials.rb', line 29

def credentials_config
  @credentials_config
end

Instance Method Details

#credentials_file_pathString

Compute the path to the credentials file.

Returns:

  • (String)

Since:

  • 14.4



58
59
60
61
62
63
64
65
# File 'lib/chef-config/mixin/credentials.rb', line 58

def credentials_file_path
  return Chef::Config[:credentials] if defined?(Chef::Config) && Chef::Config.key?(:credentials)

  env_file = ENV["CHEF_CREDENTIALS_FILE"]
  return env_file if env_file && File.file?(env_file)

  PathHelper.home(ChefUtils::Dist::Infra::USER_CONF_DIR, "credentials").freeze
end

#credentials_profile(profile = nil) ⇒ String

Compute the active credentials profile name.

The lookup order is argument (from –profile), environment variable ($CHEF_PROFILE), context file (~/.chef/context), and then “default” as a fallback.

Parameters:

  • profile (String, nil) (defaults to: nil)

    Optional override for the active profile, normally set via a command-line option.

Returns:

  • (String)

Since:

  • 14.4



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/chef-config/mixin/credentials.rb', line 41

def credentials_profile(profile = nil)
  context_file = PathHelper.home(ChefUtils::Dist::Infra::USER_CONF_DIR, "context").freeze
  if !profile.nil?
    profile
  elsif ENV.include?("CHEF_PROFILE")
    ENV["CHEF_PROFILE"]
  elsif File.file?(context_file)
    File.read(context_file).strip
  else
    "default"
  end
end

#default_secrets_providerObject

Since:

  • 13.7



167
168
169
# File 'lib/chef-config/mixin/credentials.rb', line 167

def default_secrets_provider
  global_options["default_secrets_provider"]
end

#global_optionsHash

Extract global (non-profile) settings from credentials file.

Returns:

  • (Hash)

Since:

  • 19.1



118
119
120
121
# File 'lib/chef-config/mixin/credentials.rb', line 118

def global_options
  globals = credentials_config.filter { |_, v| v.is_a? String }
  globals.merge! credentials_config.filter { |k, _| GLOBAL_CONFIG_HASHES.include? k }
end

#load_credentials(profile = nil) ⇒ void

This method returns an undefined value.

Load and process the active credentials.

Parameters:

  • profile (String, nil) (defaults to: nil)

    Optional override for the active profile, normally set via a command-line option.

See Also:

  • WorkstationConfigLoader#apply_credentials

Since:

  • 13.7



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/chef-config/mixin/credentials.rb', line 93

def load_credentials(profile = nil)
  profile = credentials_profile(profile)

  parse_credentials_file
  return if credentials_config.nil? # No credentials, nothing to do here.

  if credentials_config[profile].nil?
    # Unknown profile name. For "default" just silently ignore, otherwise
    # raise an error.
    return if profile == "default"

    raise ChefConfig::ConfigurationError, "Profile #{profile} doesn't exist. Please add it to #{credentials_file_path}."
  end

  resolve_secrets(profile)

  apply_credentials(credentials_config[profile], profile)
end

#parse_credentials_fileString?

Load and parse the credentials file.

Returns ‘nil` if the credentials file is unavailable.

Returns:

  • (String, nil)

Since:

  • 14.4



73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/chef-config/mixin/credentials.rb', line 73

def parse_credentials_file
  credentials_file = credentials_file_path
  return nil unless File.file?(credentials_file)

  begin
    @credentials_config = Tomlrb.load_file(credentials_file)
  rescue => e
    # TOML's error messages are mostly rubbish, so we'll just give a generic one
    message = "Unable to parse Credentials file: #{credentials_file}\n"
    message << e.message
    raise ChefConfig::ConfigurationError, message
  end
end

#resolve_secret(secrets_config) ⇒ String

Resolve a specific secret.

To be replaced later by a Train-like framework to support multiple backends.

Parameters:

  • secrets_config (Hash)

    Parsed contents of a secret in a profile.

Returns:

  • (String)

Since:

  • 19.1



178
179
180
# File 'lib/chef-config/mixin/credentials.rb', line 178

def resolve_secret(secrets_config)
  resolve_secret_hashicorp(secrets_config)
end

#resolve_secret_hashicorp(secrets_config) ⇒ String

Resolver logic for Hashicorp Vault.

Local lazy loading of Gems which are not part of chef-config or chef-utils, but chef itself to be switched by a unified secrets mechanism for credentials and Chef DSL later. Showstopper mitigation for 19 GA.

Parameters:

  • secrets_config (Hash)

    Parsed contents of a secret in a profile.

Returns:

  • (String)

Since:

  • 19.1



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/chef-config/mixin/credentials.rb', line 191

def resolve_secret_hashicorp(secrets_config)
  vault_config = secrets_config.transform_keys(&:to_sym)
  vault_config[:address] = vault_config[:endpoint]

  # Lazy require due to Gem being part of Chef and rarely used functionality
  require "vault" unless defined? Vault
  @vault ||= Vault::Client.new(vault_config)

  secret = secrets_config["secret"]
  engine = vault_config[:engine] || "secret"
  engine_type = vault_config[:engine_type] || "kv2"
  secret_value = case engine_type
                 when "kv", "kv1"
                   @vault.logical.read("#{engine_type}/#{secret}")
                 when "kv2"
                   @vault.kv(engine).read(secret)&.data
                 else
                   raise UnsupportedSecretsProvider.new("No support for secrets engine #{engine_type}")
                 end

  # Always JSON for Hashicorp Vault, but this is future compatible to other providers
  if secret_value.is_a?(Hash)
    require "jmespath" unless defined? ::JMESPath
    ::JMESPath.search(secrets_config["field"], secret_value)
  else
    secret_value
  end
end

#resolve_secrets(profile) ⇒ Hash

Resolve all secrets in a credentials file

Parameters:

  • profile (String)

    Profile to resolve secrets in.

Returns:

  • (Hash)

Raises:

Since:

  • 19.1



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/chef-config/mixin/credentials.rb', line 130

def resolve_secrets(profile)
  return unless credentials_config
  raise NoCredentialsFound.new("No credentials found for profile '#{profile}'") unless credentials_config[profile]

  secrets = credentials_config[profile].filter { |k, v| v.is_a?(Hash) && v.keys.include?("secret") }
  return if secrets.empty?

  secrets.each do |option, secrets_config|
    unless valid_secrets_provider?(secrets_config)
      raise UnsupportedSecretsProvider.new("Unsupported credentials secrets provider on '#{option}' for profile '#{profile}'")
    end

    secrets_config.merge!(default_secrets_provider)

    logger.debug("Resolving credentials secret '#{option}' for profile '#{profile}'")
    begin
      resolved_value = resolve_secret(secrets_config)
    ensure
      raise UnresolvedSecret.new("Could not resolve secret '#{option}' for profile '#{profile}'") if resolved_value.nil?
    end

    credentials_config[profile][option] = resolved_value
  end
end

#valid_secrets_provider?(secrets_config) ⇒ true, false

Check, if referenced secrets provider is supported.

Parameters:

  • secrets_config (Hash)

    Parsed contents of a secret in a profile.

Returns:

  • (true, false)

Since:

  • 19.1



160
161
162
163
164
165
# File 'lib/chef-config/mixin/credentials.rb', line 160

def valid_secrets_provider?(secrets_config)
  provider_config = secrets_config["secrets_provider"] || default_secrets_provider
  provider = provider_config["name"]

  provider && SUPPORTED_SECRETS_PROVIDERS.include?(provider)
end