summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarc A. Paradise <marcparadise@users.noreply.github.com>2021-07-07 17:28:37 -0400
committerGitHub <noreply@github.com>2021-07-07 17:28:37 -0400
commit7904d9f769ce8ebe5d1ec26fead20f35edf65ad0 (patch)
tree61ddc4271792a1c32d97cc048eb1fbfd2052826a
parentf06e01da751ee8b963c4aab5f0a4c31fdc18ebcc (diff)
parenta0dccdfc0f6de92c18e2b733bca8efb2f8319c91 (diff)
downloadchef-7904d9f769ce8ebe5d1ec26fead20f35edf65ad0.tar.gz
Merge pull request #11753 from chef/mp/11684-poc
Add 'secret' to the Chef DSL [beta, subject to change]
-rw-r--r--cspell.json1
-rw-r--r--lib/chef/dsl.rb1
-rw-r--r--lib/chef/dsl/secret.rb59
-rw-r--r--lib/chef/dsl/universal.rb2
-rw-r--r--lib/chef/exceptions.rb20
-rw-r--r--lib/chef/secret_fetcher.rb49
-rw-r--r--lib/chef/secret_fetcher/base.rb68
-rw-r--r--lib/chef/secret_fetcher/example.rb46
-rw-r--r--spec/unit/dsl/secret_spec.rb50
-rw-r--r--spec/unit/secret_fetcher_spec.rb74
10 files changed, 370 insertions, 0 deletions
diff --git a/cspell.json b/cspell.json
index 614bb8f06d..751a40e597 100644
--- a/cspell.json
+++ b/cspell.json
@@ -1434,6 +1434,7 @@
"secoption",
"secopts",
"secp",
+ "secretkey",
"securerandom",
"SECURITYPOLICY",
"secvalue",
diff --git a/lib/chef/dsl.rb b/lib/chef/dsl.rb
index 6893298d1d..0b43b1ae21 100644
--- a/lib/chef/dsl.rb
+++ b/lib/chef/dsl.rb
@@ -4,3 +4,4 @@ require_relative "dsl/data_query"
require_relative "dsl/include_recipe"
require_relative "dsl/include_attribute"
require_relative "dsl/registry_helper"
+require_relative "dsl/secret"
diff --git a/lib/chef/dsl/secret.rb b/lib/chef/dsl/secret.rb
new file mode 100644
index 0000000000..258bb93f14
--- /dev/null
+++ b/lib/chef/dsl/secret.rb
@@ -0,0 +1,59 @@
+#
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright (c) 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_relative "../secret_fetcher"
+
+class Chef
+ module DSL
+ module Secret
+
+ # 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]
+ #
+ # @option name [Object] The identifier or name for this secret
+ # @option service [Symbol] The service identifier for the service that will
+ # perform the secret lookup
+ # @option config [Hash] The configuration that the named service expects
+ #
+ # @example
+ #
+ # This example uses the built-in :example secret manager service, which
+ # accepts a hash of secrets.
+ #
+ # value = secret(name: "test1", service: :example, config: { "test1" => "value1" } )
+ # log "My secret is #{value}"
+ #
+ # value = secret(name: "test1", service: :aws_secrets_manager, config: { "region" => "us-west-1" })
+ # log "My secret is #{value}"
+ #
+ # @note
+ #
+ # This is pretty straightforward, but should also extend nicely to support
+ # named config (as 'service') with override config. Some future potential
+ # usage examples:
+ # value = secret(name: "test1") # If a default is configured
+ # value = secret(name: "test1", service: "my_aws_east")
+ # value = secret(name: "test1", service: "my_aws_west", config: { region: "override-region" })
+ def secret(name: nil, service: nil, config: nil)
+ Chef::SecretFetcher.for_service(service, config).fetch(name)
+ end
+ end
+ end
+end
+
+
diff --git a/lib/chef/dsl/universal.rb b/lib/chef/dsl/universal.rb
index 1e4e4218d8..4b93fa2b7b 100644
--- a/lib/chef/dsl/universal.rb
+++ b/lib/chef/dsl/universal.rb
@@ -22,6 +22,7 @@ require_relative "data_query"
require_relative "chef_vault"
require_relative "registry_helper"
require_relative "powershell"
+require_relative "secret"
require_relative "../mixin/powershell_exec"
require_relative "../mixin/powershell_out"
require_relative "../mixin/shell_out"
@@ -47,6 +48,7 @@ class Chef
include Chef::DSL::ChefVault
include Chef::DSL::RegistryHelper
include Chef::DSL::Powershell
+ include Chef::DSL::Secret
include Chef::Mixin::PowershellExec
include Chef::Mixin::PowershellOut
include Chef::Mixin::ShellOut
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index ba34b22e61..6a90b0cc52 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -290,6 +290,26 @@ class Chef
end
+ class Secret
+ class RetrievalError < RuntimeError; end
+ class ConfigurationInvalid < RuntimeError; end
+ class FetchFailed < RuntimeError; end
+ class MissingSecretName < RuntimeError; end
+
+ class InvalidFetcherService < RuntimeError
+ def initialize(given, fetcher_service_names)
+ super("#{given} is not a supported secrets service. Supported services are: :#{fetcher_service_names.join(" :")}")
+ end
+ end
+
+ class MissingFetcher < RuntimeError
+ def initialize(fetcher_service_names)
+ super("No secret service provided. Supported services are: :#{fetcher_service_names.join(" :")}")
+ end
+ end
+
+ end
+
# Exception class for collecting multiple failures. Used when running
# delayed notifications so that chef can process each delayed
# notification even if chef client or other notifications fail.
diff --git a/lib/chef/secret_fetcher.rb b/lib/chef/secret_fetcher.rb
new file mode 100644
index 0000000000..31499da0fa
--- /dev/null
+++ b/lib/chef/secret_fetcher.rb
@@ -0,0 +1,49 @@
+#
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright (c) 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_relative "exceptions"
+
+class Chef
+ class SecretFetcher
+
+ SECRET_FETCHERS = [ :example ].freeze
+
+ # Returns a configured and validated instance
+ # of a [Chef::SecretFetcher::Base] for the given
+ # service and configuration.
+ #
+ # @param service [Symbol] the identifier for the service that will support this request. Must be in
+ # SECRET_FETCHERS
+ # @param config [Hash] configuration that the secrets service requires
+ def self.for_service(service, config)
+ fetcher = case service
+ when :example
+ require_relative "secret_fetcher/example"
+ Chef::SecretFetcher::Example.new(config)
+ when nil, ""
+ raise Chef::Exceptions::Secret::MissingFetcher.new(SECRET_FETCHERS)
+ else
+ raise Chef::Exceptions::Secret::InvalidFetcherService.new("Unsupported secret service: #{service}", SECRET_FETCHERS)
+ end
+ fetcher.validate!
+ fetcher
+ end
+
+ end
+end
+
diff --git a/lib/chef/secret_fetcher/base.rb b/lib/chef/secret_fetcher/base.rb
new file mode 100644
index 0000000000..a80ecea0fb
--- /dev/null
+++ b/lib/chef/secret_fetcher/base.rb
@@ -0,0 +1,68 @@
+#
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright (c) 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_relative "../exceptions"
+
+class Chef
+ # == Chef::SecretFetcher
+ # An abstract base class that defines the methods required to implement
+ # a Secret Fetcher.
+ class SecretFetcher
+ class Base
+ attr_reader :config
+
+ # Initialize a new SecretFetcher::Base
+ #
+ # @param config [Hash] Configuration hash. Expected configuration keys and values
+ # will vary based on implementation, and are validated in `validate!`.
+ def initialize(config)
+ @config = config
+ end
+
+ # Fetch the named secret by invoking implementation-specific [Chef::SecretFetcher::Base#do_fetch]
+ #
+ # @param name [Object] the name or identifier of the secret.
+ # @note - the name parameter will probably see a narrowing of type as we learn more about different integrations.
+ # @return [Object] the result of the secret fetch
+ # @raise [Chef::Exceptions::Secret::MissingSecretName] when secret name is not provided
+ # @raise [Chef::Exceptions::Secret::FetchFailed] when the underlying attempt to fetch the secret fails.
+ def fetch(name)
+ raise Chef::Exceptions::Secret::MissingSecretName.new if name.nil? || name.to_s == ""
+
+ do_fetch(name)
+ end
+
+ # Validate that the instance is correctly configured.
+ # @raise [Chef::Exceptions::Secret::ConfigurationInvalid] if it is not.
+ def validate!; end
+
+ # Called to fetch the secret identified by 'identifer'. Implementations
+ # should expect that `validate!` has been invoked before `do_fetch`.
+ #
+ # @param identifier [Object] Unique identifier of the secret to be retrieved.
+ # When invoked via DSL, this is pre-verified to be not nil/not empty string.
+ # The expected data type and form can vary by implementation.
+ #
+ # @return [Object] The secret as returned from the implementation. The data type
+ # will vary implementation.
+ #
+ # @raise [Chef::Exceptions::Secret::FetchFailed] if the secret could not be fetched
+ def do_fetch(identifier); raise NotImplementedError.new; end
+ end
+ end
+end
diff --git a/lib/chef/secret_fetcher/example.rb b/lib/chef/secret_fetcher/example.rb
new file mode 100644
index 0000000000..7a0e671929
--- /dev/null
+++ b/lib/chef/secret_fetcher/example.rb
@@ -0,0 +1,46 @@
+#
+# Author:: Marc Paradise (<marc@chef.io>)
+# Copyright:: Copyright (c) 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_relative "base"
+
+class Chef
+ # == Chef::SecretFetcher::Example
+ # A simple implementation of a secrets fetcher.
+ # It expects to be initialized with a hash of
+ # keys and secret values.
+ #
+ # Usage Example:
+ #
+ # fetcher = SecretFetcher.for_service(:example, "secretkey1" => { "secret" => "lives here" })
+ # fetcher.fetch("secretkey1")
+ class SecretFetcher
+ class Example < Base
+ def validate!
+ if config.class != Hash
+ raise Chef::Exceptions::Secret::ConfigurationInvalid.new("The Example fetcher requires a hash of secrets")
+ end
+ end
+
+ def do_fetch(identifier)
+ raise Chef::Exceptions::Secret::FetchFailed.new("Secret #{identifier}) not found.") unless config.key?(identifier)
+
+ config[identifier]
+ end
+ end
+ end
+end
diff --git a/spec/unit/dsl/secret_spec.rb b/spec/unit/dsl/secret_spec.rb
new file mode 100644
index 0000000000..280b4f5114
--- /dev/null
+++ b/spec/unit/dsl/secret_spec.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Marc Paradise <marc@chef.io>
+# Copyright:: Copyright (c) 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 "spec_helper"
+require "chef/dsl/secret"
+require "chef/secret_fetcher/base"
+class SecretDSLTester
+ include Chef::DSL::Secret
+end
+
+class SecretFetcherImpl < Chef::SecretFetcher::Base
+ def do_fetch(name)
+ name
+ end
+end
+
+describe Chef::DSL::Secret do
+ let(:dsl) { SecretDSLTester.new }
+ it "responds to 'secret'" do
+ expect(dsl.respond_to?(:secret)).to eq true
+ end
+
+ it "uses SecretFetcher.for_service to find the fetcher" do
+ substitute_fetcher = SecretFetcherImpl.new({})
+ expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}).and_return(substitute_fetcher)
+ expect(substitute_fetcher).to receive(:fetch).with "key1"
+ 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
+end
diff --git a/spec/unit/secret_fetcher_spec.rb b/spec/unit/secret_fetcher_spec.rb
new file mode 100644
index 0000000000..3aa9efb5f1
--- /dev/null
+++ b/spec/unit/secret_fetcher_spec.rb
@@ -0,0 +1,74 @@
+#
+# Author:: Marc Paradise <marc@chef.io>
+# Copyright:: Copyright (c) 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 "chef/secret_fetcher"
+require "chef/secret_fetcher/example"
+
+class SecretFetcherImpl < Chef::SecretFetcher::Base
+ def do_fetch(name)
+ name
+ end
+
+ def validate!; end
+end
+
+describe Chef::SecretFetcher do
+ let(:fetcher_impl) { SecretFetcherImpl.new({}) }
+
+ before do
+ allow(Chef::SecretFetcher::Example).to receive(:new).and_return fetcher_impl
+ end
+
+ context ".for_service" do
+ it "resolves a known secrets service to a fetcher" do
+ Chef::SecretFetcher.for_service(:example, {})
+ end
+
+ it "raises Chef::Exceptions::Secret::MissingFetcher when service is blank" do
+ expect { Chef::SecretFetcher.for_service(nil, {}) }.to raise_error(Chef::Exceptions::Secret::MissingFetcher)
+ end
+
+ it "raises Chef::Exceptions::Secret::MissingFetcher when service is nil" do
+ expect { Chef::SecretFetcher.for_service("", {}) }.to raise_error(Chef::Exceptions::Secret::MissingFetcher)
+ end
+
+ it "raises Chef::Exceptions::Secret::InvalidFetcher for an unknown fetcher" do
+ expect { Chef::SecretFetcher.for_service(:bad_example, {}) }.to raise_error(Chef::Exceptions::Secret::InvalidFetcherService)
+ end
+
+ it "ensures fetcher configuration is valid by invoking validate!" do
+ expect(fetcher_impl).to receive(:validate!)
+ Chef::SecretFetcher.for_service(:example, {})
+ end
+ end
+
+ context "#fetch" do
+ let(:fetcher) {
+ Chef::SecretFetcher.for_service(:example, { "key1" => "value1" })
+ }
+
+ it "fetches from the underlying service when secret name is provided " do
+ expect(fetcher_impl).to receive(:fetch).with("key1")
+ fetcher.fetch("key1")
+ end
+
+ it "raises an error when the secret name is not provided" do
+ expect { fetcher.fetch(nil) }.to raise_error(Chef::Exceptions::Secret::MissingSecretName)
+ end
+ end
+end