class OpenStax::Aws::Secrets
Constants
- GENERATED_WITH_PREFIX
We store “secrets” in the AWS Parameter store.
Secrets
can be truly secret values (e.g. keys) or just some configuration values.
Attributes
client[R]
dry_run[R]
namespace[R]
region[R]
Public Class Methods
changed_secrets(existing_secrets_hash, new_secrets_array)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 82 def self.changed_secrets(existing_secrets_hash, new_secrets_array) existing_secrets_hash = existing_secrets_hash.with_indifferent_access new_secrets_array = new_secrets_array.map(&:with_indifferent_access) new_secrets_array.each_with_object([]) do |new_secret, array| existing_secret = existing_secrets_hash[new_secret[:name]] if existing_secret # No need to update if the value is the same next if existing_secret[:value] == new_secret[:value] # Don't update if different values but generated from same specification next if new_secret[:description].try(:starts_with?, GENERATED_WITH_PREFIX) && new_secret[:description] == existing_secret[:description] end array.push(new_secret) end end
new(region:, dry_run: true, namespace:)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 11 def initialize(region:, dry_run: true, namespace:) @region = region @dry_run = dry_run @namespace = namespace @client = Aws::SSM::Client.new(region: region) @substitutions = {} end
Public Instance Methods
build_secret(secret_name, spec_value, substitutions)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 123 def build_secret(secret_name, spec_value, substitutions) secret = { name: "#{key_prefix}/#{secret_name}" } case spec_value when Array processed_items = spec_value.map do |item| process_individual_spec_value(item, substitutions)[:value] end secret[:value] = processed_items.join(",") secret[:type] = "StringList" else spec_value = spec_value.to_s.strip secret.merge!(process_individual_spec_value(spec_value, substitutions)) end if (generated = secret.delete(:generated)) secret[:description] = "#{GENERATED_WITH_PREFIX} #{spec_value}" end secret end
build_secrets(specifications:, substitutions:)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 103 def build_secrets(specifications:, substitutions:) specifications ||= @specifications substitutions ||= @substitutions specifications = [specifications].flatten raise "Cannot build secrets without a specification" if specifications.empty? expanded_data = {} # later specifications override earlier ones specifications.reverse.each do |specification| expanded_data.merge!(specification.expanded_data) end expanded_data.map do |secret_name, spec_value| build_secret(secret_name, spec_value, substitutions) end end
create(specifications: nil, substitutions: nil)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 34 def create(specifications: nil, substitutions: nil) # Build all secrets first so we hit any errors before we send any to AWS built_secrets = build_secrets(specifications: specifications, substitutions: substitutions) OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run OpenStax::Aws.logger.info("Creating the following secrets in the AWS parameter store: #{built_secrets}") # Ship 'em if !dry_run built_secrets.each do |built_secret| client.put_parameter(built_secret.merge(overwrite: true)) sleep(0.1) end end end
data()
click to toggle source
# File lib/openstax/aws/secrets.rb, line 205 def data @data ||= data! end
data!()
click to toggle source
# File lib/openstax/aws/secrets.rb, line 209 def data! {}.tap do |hash| client.get_parameters_by_path({ path: key_prefix, recursive: true, with_decryption: true }).each do |response| response.parameters.each do |parameter| hash[parameter.name] = ReadOnlyParameter.new(parameter, client) end end end end
define(specifications:, substitutions: {})
click to toggle source
`create` and `update` take secrets specifications and secrets substitutions. if you just want to call `create` and `update` with no arguments, you can define the specifications and substitutions ahead of time with this method, e.g.
my_secrets.define(specifications: my_specifications, substitutions: my_substitutions) ... my_secrets.create
See the README for more discussion about specifications and substitutions.
# File lib/openstax/aws/secrets.rb, line 29 def define(specifications:, substitutions: {}) @specifications = specifications @substitutions = substitutions end
delete()
click to toggle source
# File lib/openstax/aws/secrets.rb, line 270 def delete secret_names = data!.keys return if secret_names.empty? OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run OpenStax::Aws.logger.info("Deleting the following secrets in the AWS parameter store: #{secret_names}") if !dry_run @data = nil # remove cached values as they are about to get cleared remotely # Can send max 10 secret names at a time secret_names.each_slice(10) do |some_secret_names| response = client.delete_parameters({names: some_secret_names}) if response.invalid_parameters.any? OpenStax::Aws.logger.debug("Unable to delete some secrets (likely already deleted): #{response.invalid_parameters}") end end end end
get(local_name)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 264 def get(local_name) local_name = local_name.to_s local_name = "/#{local_name}" unless local_name.chr == "/" data["#{key_prefix}#{local_name}"].try(:[], :value) end
key_prefix()
click to toggle source
# File lib/openstax/aws/secrets.rb, line 292 def key_prefix "/" + [namespace].flatten.reject(&:blank?).join("/") end
process_individual_spec_value(spec_value, substitutions)
click to toggle source
# File lib/openstax/aws/secrets.rb, line 147 def process_individual_spec_value(spec_value, substitutions) generated = false type = "SecureString" value = case spec_value when /^random\(hex,(\d+)\)$/ generated = true num_characters = $1.to_i SecureRandom.hex(num_characters)[0..num_characters-1] when /^random\(base64,(\d+)\)$/ generated = true num_characters = $1.to_i SecureRandom.urlsafe_base64(num_characters)[0..num_characters-1] when /^rsa\((\d+)\)$/ generated = true key_length = $1.to_i OpenSSL::PKey::RSA.new(key_length).to_s when "uuid" generated = true SecureRandom.uuid when /{([^{}]+)}/ spec_value.gsub(/({{\W*(\w+)\W*}})/) do |match| if (!substitutions.has_key?($2) && !substitutions.has_key?($2.to_sym)) raise "no substitution provided for #{$2}" end substitutions[$2] || substitutions[$2.to_sym] end when /^ssm\((.*)\)$/ begin parameter_name = $1.starts_with?("/") ? $1 : (substitutions[$1] || substitutions[$1.to_sym]) if parameter_name.blank? raise "#{$1} is neither a literal parameter name nor available in the given substitutions" end parameter = client.get_parameter({ name: parameter_name, with_decryption: true }).parameter type = parameter.type parameter.value rescue Aws::SSM::Errors::ParameterNotFound => ee raise "Could not get secret '#{$1}'" end else # use literal value spec_value end { value: value, type: type, generated: generated } end
update(specifications: nil, substitutions: nil, force_update_these: [])
click to toggle source
# File lib/openstax/aws/secrets.rb, line 51 def update(specifications: nil, substitutions: nil, force_update_these: []) existing_secrets = data! built_secrets = build_secrets(specifications: specifications, substitutions: substitutions) changed_secrets = self.class.changed_secrets(existing_secrets, built_secrets) force_update_these.each do |force_update_this| built_secrets.select{|built_secret| built_secret[:name].match(force_update_this)}.each do |forced| changed_secrets.push(forced) end end changed_secrets.uniq! OpenStax::Aws.logger.info("**** DRY RUN ****") if dry_run if changed_secrets.empty? OpenStax::Aws.logger.info("Secrets did not change") return false else OpenStax::Aws.logger.info("Updating the following secrets in the AWS parameter store: #{changed_secrets}") # Ship 'em if !dry_run changed_secrets.each do |changed_secret| client.put_parameter(changed_secret.merge(overwrite: true)) end end return true end end