diff options
author | Thom May <thom@chef.io> | 2017-12-12 17:32:48 +0000 |
---|---|---|
committer | Thom May <thom@chef.io> | 2017-12-13 10:17:15 +0000 |
commit | 65109e8b87c0493b76d64622b8e57679b7b909d2 (patch) | |
tree | 6ac6fc5c72b70d53069b5748bb589804c67c168c | |
parent | 2ed7c7be81b930e99affb139f1854309d55fabb5 (diff) | |
download | chef-65109e8b87c0493b76d64622b8e57679b7b909d2.tar.gz |
implement credential management
Signed-off-by: Thom May <thom@chef.io>
-rw-r--r-- | Gemfile.lock | 1 | ||||
-rw-r--r-- | chef-config/chef-config.gemspec | 1 | ||||
-rw-r--r-- | chef-config/lib/chef-config/config.rb | 7 | ||||
-rw-r--r-- | chef-config/lib/chef-config/mixin/credentials.rb | 57 | ||||
-rw-r--r-- | chef-config/lib/chef-config/workstation_config_loader.rb | 34 | ||||
-rw-r--r-- | chef-config/spec/unit/workstation_config_loader_spec.rb | 127 |
6 files changed, 226 insertions, 1 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 5e492ec332..12dc265f7e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,7 @@ PATH fuzzyurl mixlib-config (~> 2.0) mixlib-shellout (~> 2.0) + tomlrb (~> 1.2) GEM remote: https://rubygems.org/ diff --git a/chef-config/chef-config.gemspec b/chef-config/chef-config.gemspec index 9e40528fba..1dc1a118ff 100644 --- a/chef-config/chef-config.gemspec +++ b/chef-config/chef-config.gemspec @@ -19,6 +19,7 @@ Gem::Specification.new do |spec| spec.add_dependency "mixlib-config", "~> 2.0" spec.add_dependency "fuzzyurl" spec.add_dependency "addressable" + spec.add_dependency "tomlrb", "~> 1.2" spec.add_development_dependency "rake", "~> 10.0" diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb index beb78f25d0..63dd4ecda2 100644 --- a/chef-config/lib/chef-config/config.rb +++ b/chef-config/lib/chef-config/config.rb @@ -592,6 +592,12 @@ module ChefConfig # If chef-zero is enabled, this defaults to nil (no authentication). default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") } + # A credentials file may contain a complete client key, rather than the path + # to one. + # + # We'll use this preferentially. + default :client_key_contents, nil + # When registering the client, should we allow the client key location to # be a symlink? eg: /etc/chef/client.pem -> /etc/chef/prod-client.pem # If the path of the key goes through a directory like /tmp this should @@ -631,6 +637,7 @@ module ChefConfig default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") } default :validation_client_name, "chef-validator" + default :validation_key_contents, nil # When creating a new client via the validation_client account, Chef 11 # servers allow the client to generate a key pair locally and send the # public key to the server. This is more secure and helps offload work from diff --git a/chef-config/lib/chef-config/mixin/credentials.rb b/chef-config/lib/chef-config/mixin/credentials.rb new file mode 100644 index 0000000000..4c0192fff8 --- /dev/null +++ b/chef-config/lib/chef-config/mixin/credentials.rb @@ -0,0 +1,57 @@ +# +# Copyright:: Copyright 2017, Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "tomlrb" +require "chef-config/path_helper" + +module ChefConfig + module Mixin + module Credentials + + def load_credentials(profile = nil) + credentials_file = PathHelper.home(".chef", "credentials").freeze + context_file = PathHelper.home(".chef", "context").freeze + + return unless File.file?(credentials_file) + + context = File.read(context_file) if File.file?(context_file) + + environment = ENV.fetch("CHEF_PROFILE", nil) + + profile = if !profile.nil? + profile + elsif !environment.nil? + environment + elsif !context.nil? + context + else + "default" + end + + config = Tomlrb.load_file(credentials_file) + apply_credentials(config[profile], profile) + rescue ChefConfig::ConfigurationError + raise + 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 + end +end diff --git a/chef-config/lib/chef-config/workstation_config_loader.rb b/chef-config/lib/chef-config/workstation_config_loader.rb index babb78aeb8..4c07cac702 100644 --- a/chef-config/lib/chef-config/workstation_config_loader.rb +++ b/chef-config/lib/chef-config/workstation_config_loader.rb @@ -22,19 +22,23 @@ require "chef-config/logger" require "chef-config/path_helper" require "chef-config/windows" require "chef-config/mixin/dot_d" +require "chef-config/mixin/credentials" module ChefConfig class WorkstationConfigLoader include ChefConfig::Mixin::DotD + include ChefConfig::Mixin::Credentials # Path to a config file requested by user, (e.g., via command line option). Can be nil attr_accessor :explicit_config_file + attr_reader :profile # TODO: initialize this with a logger for Chef and Knife - def initialize(explicit_config_file, logger = nil) + def initialize(explicit_config_file, logger = nil, profile: nil) @explicit_config_file = explicit_config_file @chef_config_dir = nil @config_location = nil + @profile = profile @logger = logger || NullLogger.new end @@ -62,6 +66,7 @@ module ChefConfig end def load + load_credentials(profile) # Ignore it if there's no explicit_config_file and can't find one at a # default path. if !config_location.nil? @@ -138,6 +143,33 @@ module ChefConfig a end + def apply_credentials(creds, _profile) + Config.node_name = creds.fetch("node_name") if creds.key?("node_name") + Config.node_name = creds.fetch("client_name") if creds.key?("client_name") + Config.chef_server_url = creds.fetch("chef_server_url") if creds.key?("chef_server_url") + Config.validation_client_name = creds.fetch("validation_client_name") if creds.key?("validation_client_name") + + extract_key(creds, "validation_key", :validation_key, :validation_key_contents) + extract_key(creds, "validator_key", :validation_key, :validation_key_contents) + extract_key(creds, "client_key", :client_key, :client_key_contents) + end + + def extract_key(creds, name, config_path, config_contents) + return unless creds.has_key?(name) + + val = creds.fetch(name) + if val.start_with?("-----BEGIN RSA PRIVATE KEY-----") + Config.send(config_contents, val) + else + abs_path = Pathname.new(val).expand_path(home_chef_dir) + Config.send(config_path, abs_path) + end + end + + def home_chef_dir + @home_chef_dir ||= PathHelper.home(".chef") + end + def apply_config(config_content, config_file_path) Config.from_string(config_content, config_file_path) rescue SignalException diff --git a/chef-config/spec/unit/workstation_config_loader_spec.rb b/chef-config/spec/unit/workstation_config_loader_spec.rb index 087f249724..1049511285 100644 --- a/chef-config/spec/unit/workstation_config_loader_spec.rb +++ b/chef-config/spec/unit/workstation_config_loader_spec.rb @@ -363,4 +363,131 @@ RSpec.describe ChefConfig::WorkstationConfigLoader do end end end + + describe "when loading a credentials file" do + if windows? + let(:home) { "C:/Users/example.user" } + else + let(:home) { "/Users/example.user" } + end + let(:credentials_file) { "#{home}/.chef/credentials" } + let(:context_file) { "#{home}/.chef/context" } + + before do + allow(ChefConfig::PathHelper).to receive(:home).with(".chef").and_return(File.join(home, ".chef")) + allow(ChefConfig::PathHelper).to receive(:home).with(".chef", "credentials").and_return(credentials_file) + allow(ChefConfig::PathHelper).to receive(:home).with(".chef", "context").and_return(context_file) + allow(File).to receive(:file?).with(context_file).and_return false + end + + context "when the file exists" do + before do + expect(File).to receive(:read).with(credentials_file, { encoding: "utf-8" }).and_return(content) + allow(File).to receive(:file?).with(credentials_file).and_return true + end + + context "and has a default profile" do + let(:content) do + content = <<EOH +[default] +node_name = 'barney' +client_key = "barney_rubble.pem" +chef_server_url = "https://api.chef.io/organizations/bedrock" +EOH + content + end + + it "applies the expected config" do + expect { config_loader.load_credentials }.not_to raise_error + expect(ChefConfig::Config.chef_server_url).to eq("https://api.chef.io/organizations/bedrock") + expect(ChefConfig::Config.client_key.to_s).to eq("#{home}/.chef/barney_rubble.pem") + end + end + + context "and has a profile containing a full key" do + let(:content) do + content = <<EOH +[default] +client_key = """ +-----BEGIN RSA PRIVATE KEY----- +foo +""" +EOH + content + end + + it "applies the expected config" do + expect { config_loader.load_credentials }.not_to raise_error + expect(ChefConfig::Config.client_key_contents).to eq(<<EOH +-----BEGIN RSA PRIVATE KEY----- +foo +EOH +) + end + end + + context "and has several profiles" do + let(:content) do + content = <<EOH +[default] +client_name = "default" +[environment] +client_name = "environment" +[explicit] +client_name = "explicit" +[context] +client_name = "context" +EOH + content + end + + let(:env) { {} } + before do + stub_const("ENV", env) + end + + it "selects the correct profile explicitly" do + expect { config_loader.load_credentials("explicit") }.not_to raise_error + expect(ChefConfig::Config.node_name).to eq("explicit") + end + + context "with an environment variable" do + let(:env) { { "CHEF_PROFILE" => "environment" } } + + it "selects the correct profile" do + expect { config_loader.load_credentials }.not_to raise_error + expect(ChefConfig::Config.node_name).to eq("environment") + end + end + + it "selects the correct profile with a context file" do + allow(File).to receive(:file?).with(context_file).and_return true + expect(File).to receive(:read).with(context_file).and_return "context" + expect { config_loader.load_credentials }.not_to raise_error + expect(ChefConfig::Config.node_name).to eq("context") + end + + it "falls back to the default" do + expect { config_loader.load_credentials }.not_to raise_error + expect(ChefConfig::Config.node_name).to eq("default") + end + end + + context "and has a syntax error" do + let(:content) { "<<<<<" } + + it "raises a ConfigurationError" do + expect { config_loader.load_credentials }.to raise_error(ChefConfig::ConfigurationError) + end + end + end + + context "when the file does not exist" do + it "does not load anything" do + allow(File).to receive(:file?).with(credentials_file).and_return false + expect(Tomlrb).not_to receive(:load_file) + config_loader.load_credentials + end + end + end end |