summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThom May <thom@chef.io>2017-12-12 17:32:48 +0000
committerThom May <thom@chef.io>2017-12-13 10:17:15 +0000
commit65109e8b87c0493b76d64622b8e57679b7b909d2 (patch)
tree6ac6fc5c72b70d53069b5748bb589804c67c168c
parent2ed7c7be81b930e99affb139f1854309d55fabb5 (diff)
downloadchef-65109e8b87c0493b76d64622b8e57679b7b909d2.tar.gz
implement credential management
Signed-off-by: Thom May <thom@chef.io>
-rw-r--r--Gemfile.lock1
-rw-r--r--chef-config/chef-config.gemspec1
-rw-r--r--chef-config/lib/chef-config/config.rb7
-rw-r--r--chef-config/lib/chef-config/mixin/credentials.rb57
-rw-r--r--chef-config/lib/chef-config/workstation_config_loader.rb34
-rw-r--r--chef-config/spec/unit/workstation_config_loader_spec.rb127
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