Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for default secret service and config #12140

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
114 changes: 113 additions & 1 deletion lib/chef/dsl/secret.rb
Expand Up @@ -21,6 +21,118 @@ class Chef
module DSL
module Secret

#
# This allows you to set the default secret service that is used when
# fetching secrets.
#
# @example
#
# default_secret_service :hashi_vault
# val1 = secret(name: "test1", config: { region: "us-west-1" })
#
# @example
#
# default_secret_service #=> nil
# default_secret_service :hashi_vault
# default_secret_service #=> :hashi_vault
#
# @param [Symbol] service default secret service to use when fetching secrets
# @return [Symbol, nil] default secret service to use when fetching secrets
#
def default_secret_service(service = nil)
return run_context.default_secret_service if service.nil?
raise Chef::Exceptions::Secret::InvalidFetcherService.new("Unsupported secret service: #{service.inspect}", Chef::SecretFetcher::SECRET_FETCHERS) unless Chef::SecretFetcher::SECRET_FETCHERS.include?(service)

run_context.default_secret_service = service
end

#
# This allows you to set the secret service for the scope of the block
# passed into this method.
#
# @example
#
# with_secret_service :hashi_vault do
# val1 = secret(name: "test1", config: { region: "us-west-1" })
# val2 = secret(name: "test2", config: { region: "us-west-1" })
# end
#
# @example Combine with #with_secret_config
#
# with_secret_service :hashi_vault do
# with_secret_config region: "us-west-1" do
# val1 = secret(name: "test1")
# val2 = secret(name: "test2")
# end
# end
#
# @param [Symbol] service The default secret service to use when fetching secrets
#
def with_secret_service(service)
raise ArgumentError, "You must pass a block to #with_secret_service" unless block_given?

begin
old_service = default_secret_service
# Use "public" API for input validation
default_secret_service(service)
yield
ensure
# Use "private" API so we can set back to nil
run_context.default_secret_service = old_service
end
end

#
# This allows you to set the default secret config that is used when
# fetching secrets.
#
# @example
#
# default_secret_config region: "us-west-1"
# val1 = secret(name: "test1", service: :hashi_vault)
#
# @example
#
# default_secret_config #=> {}
# default_secret_service region: "us-west-1"
# default_secret_service #=> { region: "us-west-1" }
#
# @param [Hash<Symbol,Object>] config The default configuration options to apply when fetching secrets
# @return [Hash<Symbol,Object>]
#
def default_secret_config(**config)
return run_context.default_secret_config if config.empty?

run_context.default_secret_config = config
end

#
# This allows you to set the secret config for the scope of the block
# passed into this method.
#
# @example
#
# with_secret_config region: "us-west-1" do
# val1 = secret(name: "test1", service: :hashi_vault)
# val2 = secret(name: "test2", service: :hashi_vault)
# end
#
# @param [Hash<Symbol,Object>] config The default configuration options to use when fetching secrets
#
def with_secret_config(**config)
raise ArgumentError, "You must pass a block to #with_secret_config" unless block_given?

begin
old_config = default_secret_config
# Use "public" API for input validation
default_secret_config(**config)
yield
ensure
# Use "private" API so we can set back to nil
run_context.default_secret_config = old_config
end
end

# Helper method which looks up a secret using the given service and configuration,
# and returns the retrieved secret value.
# This DSL providers a wrapper around [Chef::SecretFetcher]
Expand Down Expand Up @@ -49,7 +161,7 @@ module Secret
#
# value = secret(name: "test1", service: :aws_secrets_manager, version: "v1", config: { region: "us-west-1" })
# log "My secret is #{value}"
def secret(name: nil, version: nil, service: nil, config: {})
def secret(name: nil, version: nil, service: default_secret_service, config: default_secret_config)
Chef::Log.warn <<~EOM.gsub("\n", " ")
The secrets Chef Infra language helper is currently in beta. If you have feedback or you would
like to be part of the future design of this helper e-mail us at secrets_management_beta@progress.com"
Expand Down
16 changes: 16 additions & 0 deletions lib/chef/run_context.rb
Expand Up @@ -145,6 +145,16 @@ def root_run_context
#
attr_accessor :input_collection

#
# @return [Symbol, nil]
#
attr_accessor :default_secret_service

#
# @return [Hash<Symbol,Object>]
#
attr_accessor :default_secret_config

# Pointer back to the Chef::Runner that created this
#
attr_accessor :runner
Expand Down Expand Up @@ -222,6 +232,8 @@ def initialize(node = nil, cookbook_collection = nil, events = nil, logger = nil
@input_collection = Chef::Compliance::InputCollection.new(events)
@waiver_collection = Chef::Compliance::WaiverCollection.new(events)
@profile_collection = Chef::Compliance::ProfileCollection.new(events)
@default_secret_service = nil
@default_secret_config = {}

initialize_child_state
end
Expand Down Expand Up @@ -693,6 +705,10 @@ class ChildRunContext < RunContext
cookbook_collection
cookbook_collection=
cookbook_compiler
default_secret_config
default_secret_config=
default_secret_service
default_secret_service=
definitions
events
events=
Expand Down
150 changes: 127 additions & 23 deletions spec/unit/dsl/secret_spec.rb
Expand Up @@ -17,11 +17,14 @@
#

require "spec_helper"
require "chef/exceptions"
require "chef/dsl/secret"
require "chef/secret_fetcher/base"

class SecretDSLTester
include Chef::DSL::Secret
# Because DSL is invoked in the context of a recipe,

# Because DSL is invoked in the context of a recipe or attribute file
# we expect run_context to always be available when SecretFetcher::Base
# requests it - making it safe to mock here
def run_context
Expand All @@ -37,35 +40,136 @@ def do_fetch(name, version)

describe Chef::DSL::Secret do
let(:dsl) { SecretDSLTester.new }
it "responds to 'secret'" do
expect(dsl.respond_to?(:secret)).to eq true
let(:run_context) { Chef::RunContext.new(Chef::Node.new, {}, Chef::EventDispatch::Dispatcher.new) }

before do
allow(dsl).to receive(:run_context).and_return(run_context)
end

it "uses SecretFetcher.for_service to find the fetcher" do
substitute_fetcher = SecretFetcherImpl.new({}, nil)
expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, nil).and_return(substitute_fetcher)
expect(substitute_fetcher).to receive(:fetch).with("key1", nil)
dsl.secret(name: "key1", service: :example, config: {})
%w{
secret
default_secret_service
default_secret_config
with_secret_service
with_secret_config
}.each do |m|
it "responds to ##{m}" do
expect(dsl.respond_to?(m)).to eq true
end
end

describe "#default_secret_service" do
let(:service) { :hashi_vault }

it "persists the service passed in as an argument" do
expect(dsl.default_secret_service).to eq(nil)
dsl.default_secret_service(service)
expect(dsl.default_secret_service).to eq(service)
end

it "returns run_context.default_secret_service value when no argument is given" do
run_context.default_secret_service = :my_thing
expect(dsl.default_secret_service).to eq(:my_thing)
end

it "raises exception when service given is not valid" do
stub_const("Chef::SecretFetcher::SECRET_FETCHERS", %i{service_a service_b})
expect { dsl.default_secret_service(:unknown_service) }.to raise_error(Chef::Exceptions::Secret::InvalidFetcherService)
end
end

it "resolves a secret when using the example fetcher" do
secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" })
expect(secret_value).to eq "secret value"
describe "#with_secret_config" do
let(:service) { :hashi_vault }

it "sets the service for the scope of the block only" do
expect(dsl.default_secret_service).to eq(nil)
dsl.with_secret_service(service) do
expect(dsl.default_secret_service).to eq(service)
end
expect(dsl.default_secret_service).to eq(nil)
end

it "raises exception when block is not given" do
expect { dsl.with_secret_service(service) }.to raise_error(ArgumentError)
end
end

context "when used within a resource" do
let(:run_context) {
Chef::RunContext.new(Chef::Node.new,
Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))),
Chef::EventDispatch::Dispatcher.new)
}

it "marks that resource as 'sensitive'" do
recipe = Chef::Recipe.new("secrets", "test", run_context)
recipe.zen_master "secret_test" do
peace secret(name: "test1", service: :example, config: { "test1" => true })
describe "#default_secret_config" do
let(:config) { { my_key: "value" } }

it "persists the config passed in as argument" do
expect(dsl.default_secret_config).to eq({})
dsl.default_secret_config(**config)
expect(dsl.default_secret_config).to eq(config)
end

it "returns run_context.default_secret_config value when no argument is given" do
run_context.default_secret_config = { my_thing: "that" }
expect(dsl.default_secret_config).to eq({ my_thing: "that" })
end
end

describe "#with_secret_config" do
let(:config) { { my_key: "value" } }

it "sets the config for the scope of the block only" do
expect(dsl.default_secret_config).to eq({})
dsl.with_secret_config(**config) do
expect(dsl.default_secret_config).to eq(config)
end
expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true)
expect(dsl.default_secret_config).to eq({})
end

it "raises exception when block is not given" do
expect { dsl.with_secret_config(**config) }.to raise_error(ArgumentError)
end
end

describe "#secret" do
it "uses SecretFetcher.for_service to find the fetcher" do
substitute_fetcher = SecretFetcherImpl.new({}, nil)
expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, run_context).and_return(substitute_fetcher)
expect(substitute_fetcher).to receive(:fetch).with("key1", nil)
dsl.secret(name: "key1", service: :example, config: {})
end

it "resolves a secret when using the example fetcher" do
secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" })
expect(secret_value).to eq "secret value"
end

context "when used within a resource" do
let(:run_context) {
Chef::RunContext.new(Chef::Node.new,
Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))),
Chef::EventDispatch::Dispatcher.new)
}

it "marks that resource as 'sensitive'" do
recipe = Chef::Recipe.new("secrets", "test", run_context)
recipe.zen_master "secret_test" do
peace secret(name: "test1", service: :example, config: { "test1" => true })
end
expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true)
end
end

it "passes default service to SecretFetcher.for_service" do
service = :example
dsl.default_secret_service(service)
substitute_fetcher = SecretFetcherImpl.new({}, nil)
expect(Chef::SecretFetcher).to receive(:for_service).with(service, {}, run_context).and_return(substitute_fetcher)
allow(substitute_fetcher).to receive(:fetch).with("key1", nil)
dsl.secret(name: "key1")
end

it "passes default config to SecretFetcher.for_service" do
config = { my_config: "value" }
dsl.default_secret_config(**config)
substitute_fetcher = SecretFetcherImpl.new({}, nil)
expect(Chef::SecretFetcher).to receive(:for_service).with(:example, config, run_context).and_return(substitute_fetcher)
allow(substitute_fetcher).to receive(:fetch).with("key1", nil)
dsl.secret(name: "key1", service: :example)
end
end
end
16 changes: 16 additions & 0 deletions spec/unit/run_context_spec.rb
Expand Up @@ -53,6 +53,22 @@
expect(run_context.node).to eq(node)
end

it "responds to #default_secret_service" do
expect(run_context).to respond_to(:default_secret_service)
end

it "responds to #default_secret_config" do
expect(run_context).to respond_to(:default_secret_config)
end

it "#default_secret_service defaults to nil" do
expect(run_context.default_secret_service).to eq(nil)
end

it "#default_secret_config defaults to {}" do
expect(run_context.default_secret_config).to eq({})
end

it "loads up node[:cookbooks]" do
expect(run_context.node[:cookbooks]).to eql(
{
Expand Down