Skip to content

Commit

Permalink
Allow decryption before lookup #264
Browse files Browse the repository at this point in the history
  • Loading branch information
oneiros committed Jan 31, 2024
1 parent 3cf15cf commit c80388a
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 34 deletions.
34 changes: 16 additions & 18 deletions app/models/hiera_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion app/models/hiera_data/data_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions app/models/hiera_data/e_yaml_file.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions app/models/hiera_data/hierarchy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) &&
Expand Down
9 changes: 9 additions & 0 deletions app/models/hiera_data/util.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class HieraData
module Util
module_function

def yaml_format(value)
value.to_yaml.sub(/\A---(\n| )/, '').chomp
end
end
end
6 changes: 3 additions & 3 deletions app/models/hiera_data/yaml_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion app/models/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions app/models/value.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
class Value < HieraModel
ENCRYPTED_PATTERN = /.*ENC\[.*\]/

attribute :data_file
attribute :key
attribute :value, :string
Expand All @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions app/views/lookups/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
<%= turbo_frame_tag "lookup-result-frame" do %>
<code><pre>
<%= @result.to_yaml.sub(/^---$/, "") %>
</pre></code>
<code><pre><%= HieraData::Util.yaml_format(@result) %></pre></code>
<% end %>
51 changes: 51 additions & 0 deletions test/models/hiera_data/e_yaml_file_test.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions test/models/hiera_data/util_test.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions test/models/hiera_data/yaml_file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -131,7 +131,7 @@ def config_dir
end

def key_as_string
<<~HEREDOC
<<~HEREDOC.chomp
tp::conf:
postfix:
template: foobar/postfix/main.cf.epp
Expand Down
6 changes: 3 additions & 3 deletions test/models/hiera_data_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"}

Expand Down

0 comments on commit c80388a

Please sign in to comment.