Skip to content

Commit

Permalink
Merge pull request #12856 from jasonwbarnett/jwb/backport-default-sec…
Browse files Browse the repository at this point in the history
…ret-api

Backport #12140 to chef-17
  • Loading branch information
marcparadise committed May 10, 2022
2 parents 630f79c + 2179c04 commit 6b4e4df
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 24 deletions.
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

0 comments on commit 6b4e4df

Please sign in to comment.