Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow doing lookups #283

Merged
merged 10 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,6 @@ Style/BlockDelimiters:
Style/ClassAndModuleChildren:
Exclude:
- 'test/channels/application_cable/connection_test.rb'
- 'test/models/hiera_data/config_test.rb'
- 'test/models/hiera_data/data_file_test.rb'
- 'test/models/hiera_data/git_repo_test.rb'
- 'test/models/hiera_data/interpolation_test.rb'
- 'test/models/hiera_data/yaml_file_test.rb'

# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ gem 'net-ldap', require: "net/ldap"
gem 'puppet'
gem 'puppetdb-ruby', require: 'puppetdb'
gem 'ruby-saml'
gem 'deep_merge', require: "deep_merge/core"

# To use retry middleware with Faraday v2.0+
gem 'faraday-retry'
Expand Down
9 changes: 5 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,10 @@ GEM
rake (>= 10.0)
globalid (1.2.1)
activesupport (>= 6.1)
google-protobuf (3.25.2)
google-protobuf (3.25.2-arm64-darwin)
google-protobuf (3.25.2-x86_64-darwin)
google-protobuf (3.25.2-x86_64-linux)
google-protobuf (3.25.3)
google-protobuf (3.25.3-arm64-darwin)
google-protobuf (3.25.3-x86_64-darwin)
google-protobuf (3.25.3-x86_64-linux)
hiera-eyaml (3.4.0)
highline
optimist
Expand Down Expand Up @@ -479,6 +479,7 @@ DEPENDENCIES
capybara (>= 2.15)
dartsass-rails
dartsass-sprockets
deep_merge
diffy
factory_bot_rails
faker
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ def load_environments
authorize! :show, @environment
end

def load_nodes
@nodes = Node.all
@nodes.select! { |n| current_user.may_access?(n) }
@node = Node.find(params[:node_id])
authorize! :show, @node
end

def display_error_page(error)
@error = error
render template: "page/error", status: :internal_server_error
Expand Down
9 changes: 0 additions & 9 deletions app/controllers/keys_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,4 @@ def destroy
redirect_to environment_node_key_path(@environment, @node, @key),
notice: "Value was removed successfully"
end

private

def load_nodes
@nodes = Node.all
@nodes.select! { |n| current_user.may_access?(n) }
@node = Node.find(params[:node_id])
authorize! :show, @node
end
end
22 changes: 22 additions & 0 deletions app/controllers/lookups_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class LookupsController < ApplicationController
before_action :load_environments
before_action :load_nodes
before_action :load_keys

def show
@result = @key.lookup(@node)
rescue Hdm::Error => e
@error = e

render action: "error"
end

private

def load_keys
@keys = Key.all_for(@node, environment: @environment)
@keys.select! { |k| current_user.may_access?(k) }
@key = Key.new(environment: @environment, name: params[:key_id])
authorize! :show, @key
end
end
67 changes: 47 additions & 20 deletions app/models/hiera_data.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,32 +107,35 @@ 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: {}, decrypt: false)
candidates = lookup_for(facts, decrypt:)
.lookup("lookup_options", merge_strategy: :hash)
merge = extract_merge_value(key, candidates)
case merge
when String
merge
when Hash
merge["strategy"]
else
"first"
end
end

def lookup_options(facts)
result = {}
config.hierarchies.each do |hierarchy|
hierarchy.resolved_paths(facts:).each do |path|
file = DataFile.new(path: hierarchy.datadir(facts:).join(path))
result = (file["lookup_options"] || {}).merge(result)
end
end
result
def lookup(key, facts: {}, decrypt: false)
merge_strategy = lookup_options_for(key, facts:, decrypt:).to_sym
lookup_for(facts).lookup(key, merge_strategy:)
end

private
Expand All @@ -145,5 +148,29 @@ def config
def find_hierarchy(name)
config.hierarchies.find { |h| h.name == name }
end

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),
type: hierarchy.backend,
options: hierarchy.file_options.merge({ decrypt: })
).content
end
end
Lookup.new(hashes.compact)
end
end

def extract_merge_value(key, candidates)
result = candidates&.dig(key)
result ||= candidates&.select { |k, _v| k[0] == "^" }
&.find { |k, _v| key.match(Regexp.new(k)) }
&.last
result&.dig("merge")
end
end
# rubocop:enable Metrics/ClassLength
8 changes: 6 additions & 2 deletions app/models/hiera_data/data_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ class HieraData
class DataFile
attr_reader :path, :file

delegate :exist?, :writable?, :keys, :content_for_key, :[],
delegate :content, :exist?, :writable?, :keys,
: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 @@ -58,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:,
private_key:
}.compact
end

def encryptable?
backend == :eyaml &&
File.readable?(private_key) &&
Expand Down
57 changes: 57 additions & 0 deletions app/models/hiera_data/lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
class HieraData
class Lookup
def initialize(hashes)
@hashes = hashes
end

def lookup(key, merge_strategy: :first)
applicable_values = extract_key_from_hashes(key)
merge_values(applicable_values, merge_strategy)
end

private

def extract_key_from_hashes(key)
@hashes
.select { |h| h.key?(key) }
.pluck(key)
end

def merge_values(values, strategy)
case strategy
when :first
values.first
when :unique
unique_array_merge(values)
when :hash
flat_hash_merge(values)
when :deep
deep_merge(values)
end
end

def unique_array_merge(values)
values.inject([]) do |memo, value|
raise Hdm::Error, "Merge strategy `unique` is not applicable to a hash." if value.is_a?(Hash)

memo + Array(value)
end.uniq
end

def flat_hash_merge(values)
values.inject({}) do |memo, value|
raise Hdm::Error, "Merge strategy `hash` can only be used with hashes." unless value.is_a?(Hash)

value.merge(memo)
end
end

def deep_merge(values)
values.inject do |memo, value|
memo ||= {}

DeepMerge.deep_merge!(memo, value, merge_hash_arrays: true)
end
end
end
end
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
Loading