diff --git a/app/models/hiera_data.rb b/app/models/hiera_data.rb index dd1945ef..704b10ff 100644 --- a/app/models/hiera_data.rb +++ b/app/models/hiera_data.rb @@ -107,25 +107,20 @@ def remove_key(hierarchy_name, path, key, facts: {}) def decrypt_value(hierarchy_name, value) hierarchy = find_hierarchy(hierarchy_name) - Hiera::Backend::Eyaml::Options["pkcs7_private_key"] = hierarchy.private_key - Hiera::Backend::Eyaml::Options["pkcs7_public_key"] = hierarchy.public_key - parser = Hiera::Backend::Eyaml::Parser::Parser.new([Hiera::Backend::Eyaml::Parser::EncHieraTokenType.new, Hiera::Backend::Eyaml::Parser::EncBlockTokenType.new]) - tokens = parser.parse(value) - tokens.map(&:to_plain_text).join + public_key = hierarchy.public_key + private_key = hierarchy.private_key + EYamlFile.decrypt_value(value, public_key:, private_key:) end def encrypt_value(hierarchy_name, value) hierarchy = find_hierarchy(hierarchy_name) - Hiera::Backend::Eyaml::Options["pkcs7_private_key"] = hierarchy.private_key - Hiera::Backend::Eyaml::Options["pkcs7_public_key"] = hierarchy.public_key - encryptor = Hiera::Backend::Eyaml::Encryptor.find - ciphertext = encryptor.encode(encryptor.encrypt(value)) - token = Hiera::Backend::Eyaml::Parser::EncToken.new(:block, value, encryptor, ciphertext, nil, ' ') - token.to_encrypted format: :string + public_key = hierarchy.public_key + private_key = hierarchy.private_key + EYamlFile.encrypt_value(value, public_key:, private_key:) end - def lookup_options_for(key, facts: {}) - candidates = lookup_for(facts) + def lookup_options_for(key, facts: {}, decrypt: false) + candidates = lookup_for(facts, decrypt:) .lookup("lookup_options", merge_strategy: :hash) merge = extract_merge_value(key, candidates) case merge @@ -138,8 +133,8 @@ def lookup_options_for(key, facts: {}) end end - def lookup(key, facts: {}) - merge_strategy = lookup_options_for(key, facts:).to_sym + def lookup(key, facts: {}, decrypt: false) + merge_strategy = lookup_options_for(key, facts:, decrypt:).to_sym lookup_for(facts).lookup(key, merge_strategy:) end @@ -154,13 +149,16 @@ def find_hierarchy(name) config.hierarchies.find { |h| h.name == name } end - def lookup_for(facts) + def lookup_for(facts, decrypt: false) @cached_lookups ||= {} @cached_lookups[facts] ||= begin hashes = config.hierarchies.flat_map do |hierarchy| hierarchy.resolved_paths(facts:).map do |path| - DataFile.new(path: hierarchy.datadir(facts:).join(path)) - .content + DataFile.new( + path: hierarchy.datadir(facts:).join(path), + type: hierarchy.backend, + options: hierarchy.file_options.merge({decrypt: decrypt}) + ).content end end Lookup.new(hashes.compact) diff --git a/app/models/hiera_data/data_file.rb b/app/models/hiera_data/data_file.rb index fcae9c2c..1c77da4c 100644 --- a/app/models/hiera_data/data_file.rb +++ b/app/models/hiera_data/data_file.rb @@ -6,9 +6,10 @@ class DataFile :content_for_key, :[], to: :file - def initialize(path:, facts: {}, type: :yaml) + def initialize(path:, facts: {}, options: {}, type: :yaml) @path = path @facts = facts + @options = options @replaced_from_git = false setup_git_location @file = create_file(type) @@ -59,6 +60,8 @@ def matching_git_location def create_file(type) case type + when :eyaml + EYamlFile.new(path: @path, options: @options) when :yaml YamlFile.new(path: @path) else diff --git a/app/models/hiera_data/e_yaml_file.rb b/app/models/hiera_data/e_yaml_file.rb new file mode 100644 index 00000000..8f9be2f2 --- /dev/null +++ b/app/models/hiera_data/e_yaml_file.rb @@ -0,0 +1,55 @@ +class HieraData + class EYamlFile < YamlFile + ENCRYPTED_PATTERN = /.*ENC\[.*\]/ + + def self.encrypted?(value) + value.is_a?(String) && !!value.match(ENCRYPTED_PATTERN) + end + + def self.decrypt_value(value, public_key:, private_key:) + Hiera::Backend::Eyaml::Options["pkcs7_private_key"] = private_key + Hiera::Backend::Eyaml::Options["pkcs7_public_key"] = public_key + parser = Hiera::Backend::Eyaml::Parser::Parser.new([Hiera::Backend::Eyaml::Parser::EncHieraTokenType.new, Hiera::Backend::Eyaml::Parser::EncBlockTokenType.new]) + tokens = parser.parse(value) + tokens.map(&:to_plain_text).join + end + + def self.encrypt_value(value, public_key:, private_key:) + Hiera::Backend::Eyaml::Options["pkcs7_private_key"] = private_key + Hiera::Backend::Eyaml::Options["pkcs7_public_key"] = public_key + encryptor = Hiera::Backend::Eyaml::Encryptor.find + ciphertext = encryptor.encode(encryptor.encrypt(value)) + token = Hiera::Backend::Eyaml::Parser::EncToken.new(:block, value, encryptor, ciphertext, nil, ' ') + token.to_encrypted format: :string + end + + def content + @content ||= + begin + super + decrypt_content if @content && decrypt? + @content + end + end + + private + + def decrypt_content + public_key = @options[:public_key] + private_key = @options[:private_key] + @content.transform_values! do |value| + if self.class.encrypted?(value) + self.class.decrypt_value(value, public_key:, private_key:) + else + value + end + end + end + + def decrypt? + @options[:decrypt] && + @options[:public_key] && + @options[:private_key] + end + end +end diff --git a/app/models/hiera_data/hierarchy.rb b/app/models/hiera_data/hierarchy.rb index d532dc20..6a3df190 100644 --- a/app/models/hiera_data/hierarchy.rb +++ b/app/models/hiera_data/hierarchy.rb @@ -55,6 +55,13 @@ def public_key @base_path.join(raw_hash.dig("options", "pkcs7_public_key")) end + def file_options + { + public_key: public_key, + private_key: private_key + }.compact + end + def encryptable? backend == :eyaml && File.readable?(private_key) && diff --git a/app/models/hiera_data/util.rb b/app/models/hiera_data/util.rb new file mode 100644 index 00000000..96b8307c --- /dev/null +++ b/app/models/hiera_data/util.rb @@ -0,0 +1,9 @@ +class HieraData + module Util + module_function + + def yaml_format(value) + value.to_yaml.sub(/\A---(\n| )/, '').chomp + end + end +end diff --git a/app/models/hiera_data/yaml_file.rb b/app/models/hiera_data/yaml_file.rb index 436161f1..24fc7a24 100644 --- a/app/models/hiera_data/yaml_file.rb +++ b/app/models/hiera_data/yaml_file.rb @@ -2,8 +2,9 @@ class HieraData class YamlFile attr_reader :path - def initialize(path:) + def initialize(path:, options: {}) @path = path + @options = options end def exist? @@ -46,8 +47,7 @@ def content_for_key(key) return "false" if value == false return value if [String, Integer, Float].include?(value.class) - value_string = value.to_yaml - value_string.sub(/\A---(\n| )/, '').gsub(/^$\n/, '') + Util.yaml_format(value) end def write_key(key, value) diff --git a/app/models/key.rb b/app/models/key.rb index 6dcf4fa7..73b914d7 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -29,7 +29,9 @@ def lookup_options(node) end def lookup(node) - hiera_data.lookup(name, facts: node.facts) + hiera_data.lookup(name, + facts: node.facts, + decrypt: Rails.configuration.hdm.allow_encryption) end def to_param diff --git a/app/models/value.rb b/app/models/value.rb index 19e670b0..75185899 100644 --- a/app/models/value.rb +++ b/app/models/value.rb @@ -1,6 +1,4 @@ class Value < HieraModel - ENCRYPTED_PATTERN = /.*ENC\[.*\]/ - attribute :data_file attribute :key attribute :value, :string @@ -9,7 +7,7 @@ class Value < HieraModel delegate :environment, to: :hierarchy def encrypted? - value&.match(ENCRYPTED_PATTERN) + HieraData::EYamlFile.encrypted?(value) end def update(new_value, node: nil) diff --git a/app/views/lookups/show.html.erb b/app/views/lookups/show.html.erb index 0225ad1e..65526e1c 100644 --- a/app/views/lookups/show.html.erb +++ b/app/views/lookups/show.html.erb @@ -1,5 +1,3 @@ <%= turbo_frame_tag "lookup-result-frame" do %> -
-    <%= @result.to_yaml.sub(/^---$/, "") %>
-  
+
<%= HieraData::Util.yaml_format(@result) %>
<% end %> diff --git a/test/models/hiera_data/e_yaml_file_test.rb b/test/models/hiera_data/e_yaml_file_test.rb new file mode 100644 index 00000000..c0aed2bd --- /dev/null +++ b/test/models/hiera_data/e_yaml_file_test.rb @@ -0,0 +1,51 @@ +require 'test_helper' + +class HieraData + class EYamlFileTest < ActiveSupport::TestCase + test "::encrypted? detects encrypted values" do + assert_equal false, EYamlFile.encrypted?("string") + + assert_equal true, EYamlFile.encrypted?(ciphertext) + end + + test "::decrypt_value can decrypt a value" do + assert_equal "top secret", EYamlFile.decrypt_value(ciphertext, public_key:, private_key:) + end + + test "::encrypt_value can encrypt values" do + encrypted_value = EYamlFile.encrypt_value("top secret", public_key:, private_key:) + + assert_match /\AENC\[.+\]\z/, encrypted_value + assert_no_match /top secret/, encrypted_value + end + + test "#content can decrypt top level encrypted values" do + file = EYamlFile.new( + path: config_dir.join("common.yaml"), + options: { + public_key:, private_key: , decrypt: true + } + ) + + assert_equal "c8hoduj5", file.content["c8hoduj5"].chomp + end + + private + + def ciphertext + "ENC[PKCS7,MIIBeQYJKoZIhvcNAQcDoIIBajCCAWYCAQAxggEhMIIBHQIBADAFMAACAQEwDQYJKoZIhvcNAQEBBQAEggEAe2qPOZxi519fmMyaH47BN1oEnDcluk5ec0jlugSzyInd3v2qirncMYVcAvjg2ckjhWX4h458ZJJuDpT5+ediNG+OQ/BAO+QgjHu7eAR8imjBmeFbjN+dl90y4Lh0S4b/ihpcJ8N9qASWvCePmKafjwFaKNjc6Dws05OQ+G/oBIiXGkXJsE6kbT1qX9DrovHEO6Ve2dANUYmiw1oC8cyqSPi8aBeDdBmZJCQyDrx37QTXf8+b0aVAMG4KPEI1vdoO10ElAsof8Mwx60HkUCCSXRZ2fACp5ODf+hgg9B7Z4eFRxIf4VuqPI+b4pcvPRS/PExI2E99YXIyJz86DD7KPFjA8BgkqhkiG9w0BBwEwHQYJYIZIAWUDBAEqBBAgGnfhv3yX43m4aHwqBAB9gBAHgnAZ17HQe3wMCQ2pPuh8]" + end + + def config_dir + Pathname.new(Rails.configuration.hdm["config_dir"]).join('environments', 'eyaml', 'data') + end + + def public_key + Rails.root.join("test/fixtures/files/puppet/environments/eyaml/keys/public_key.pkcs7.pem") + end + + def private_key + Rails.root.join("test/fixtures/files/puppet/environments/eyaml/keys/private_key.pkcs7.pem") + end + end +end diff --git a/test/models/hiera_data/util_test.rb b/test/models/hiera_data/util_test.rb new file mode 100644 index 00000000..4fe030fb --- /dev/null +++ b/test/models/hiera_data/util_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class HieraData + class UtilTest < ActiveSupport::TestCase + test "::yaml_format converts a hash to yaml and removes leading `---`" do + hash = { "test" => 23 } + + assert_equal "test: 23", Util.yaml_format(hash) + end + + test "::yaml_format converts an integer to yaml and removes leading `---`" do + integer = 23 + + assert_equal "23", Util.yaml_format(integer) + end + end +end diff --git a/test/models/hiera_data/yaml_file_test.rb b/test/models/hiera_data/yaml_file_test.rb index 8b60ffca..66eaae7d 100644 --- a/test/models/hiera_data/yaml_file_test.rb +++ b/test/models/hiera_data/yaml_file_test.rb @@ -75,7 +75,7 @@ class YamlFileTest < ActiveSupport::TestCase test "#content_for_key returns empty array as array" do file = HieraData::YamlFile.new(path: config_dir.join("nodes/testhost.yaml")) - assert_equal "[]\n", file.content_for_key('classes') + assert_equal "[]", file.content_for_key('classes') end test "#write_key goes fine" do @@ -131,7 +131,7 @@ def config_dir end def key_as_string - <<~HEREDOC + <<~HEREDOC.chomp tp::conf: postfix: template: foobar/postfix/main.cf.epp diff --git a/test/models/hiera_data_test.rb b/test/models/hiera_data_test.rb index 74384bdb..ea305cc0 100644 --- a/test/models/hiera_data_test.rb +++ b/test/models/hiera_data_test.rb @@ -13,11 +13,11 @@ class HieraDataTest < ActiveSupport::TestCase test "#search_key returns key data for all given files" do hiera = HieraData.new('development') expected_result = { - "nodes/testhost.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: hostname\n"}, + "nodes/testhost.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: hostname"}, "role/hdm_test-development.yaml" => {file_present: false, file_writable: true, replaced_from_git: false, key_present: false, value: nil}, - "role/hdm_test.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: hostname-role\n"}, + "role/hdm_test.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: hostname-role"}, "zone/internal.yaml" => {file_present: false, file_writable: false, replaced_from_git: false, key_present: false, value: nil }, - "common.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: common::hostname\n"} + "common.yaml" => {file_present: true, file_writable: true, replaced_from_git: false, key_present: true, value: "hostname: common::hostname"} } facts = {"fqdn" => "testhost", "role" => "hdm_test", "env" => "development", "zone" => "internal"}