|
| 1 | +# Originally copied from Peter Souter hiera_vault repository, licensed under the Apache License, Version 2.0 |
| 2 | +# |
| 3 | +# https://github.com/petems/petems-hiera_vault/blob/master/lib/puppet/functions/hiera_vault.rb |
| 4 | + |
| 5 | +require_relative 'hiera_vault/authentication.rb' |
| 6 | + |
| 7 | +Puppet::Functions.create_function(:hiera_vault) do |
| 8 | + begin |
| 9 | + require 'vault' |
| 10 | + rescue LoadError => _e |
| 11 | + raise Puppet::DataBinding::LookupError, "[hiera-vault] Must install vault gem to use hiera-vault backend" |
| 12 | + end |
| 13 | + |
| 14 | + dispatch :lookup_key do |
| 15 | + param 'Variant[String, Numeric]', :key |
| 16 | + param 'Hash', :options |
| 17 | + param 'Puppet::LookupContext', :context |
| 18 | + end |
| 19 | + |
| 20 | + def lookup_key(key, options, context) |
| 21 | + if confine_keys = options['confine_to_keys'] |
| 22 | + raise ArgumentError, '[hiera-vault] confine_to_keys must be an array' unless confine_keys.is_a?(Array) |
| 23 | + |
| 24 | + begin |
| 25 | + confine_keys = confine_keys.map { |r| Regexp.new(r) } |
| 26 | + rescue StandardError => e |
| 27 | + raise Puppet::DataBinding::LookupError, "[hiera-vault] creating regexp failed with: #{e}" |
| 28 | + end |
| 29 | + |
| 30 | + regex_key_match = Regexp.union(confine_keys) |
| 31 | + |
| 32 | + unless key[regex_key_match] == key |
| 33 | + context.explain { "[hiera-vault] Skipping hiera_vault backend because key '#{key}' does not match confine_to_keys" } |
| 34 | + context.not_found |
| 35 | + end |
| 36 | + end |
| 37 | + |
| 38 | + if strip_from_keys = options['strip_from_keys'] |
| 39 | + raise ArgumentError, '[hiera-vault] strip_from_keys must be an array' unless strip_from_keys.is_a?(Array) |
| 40 | + |
| 41 | + strip_from_keys.each do |prefix| |
| 42 | + key = key.gsub(Regexp.new(prefix), '') |
| 43 | + end |
| 44 | + end |
| 45 | + |
| 46 | + vault_get(key, options, context) |
| 47 | + end |
| 48 | + |
| 49 | + def vault_get(key, options, context) |
| 50 | + hiera_vault_client = Vault::Client.new |
| 51 | + |
| 52 | + begin |
| 53 | + hiera_vault_client.configure do |config| |
| 54 | + config.address = options['address'] unless options['address'].nil? |
| 55 | + config.ssl_verify = options['ssl_verify'] unless options['ssl_verify'].nil? |
| 56 | + config.ssl_ca_cert = options['ssl_ca_cert'] if config.respond_to? :ssl_ca_cert |
| 57 | + end |
| 58 | + |
| 59 | + context.explain { "[hiera-vault] Using #{options['authentication']['method']} authentication" } |
| 60 | + authenticate(options['authentication'], hiera_vault_client, context) |
| 61 | + |
| 62 | + if hiera_vault_client.sys.seal_status.sealed? |
| 63 | + raise Puppet::DataBinding::LookupError, "[hiera-vault] vault is sealed" |
| 64 | + end |
| 65 | + |
| 66 | + context.explain { "[hiera-vault] Client configured to connect to #{hiera_vault_client.address}" } |
| 67 | + rescue StandardError => e |
| 68 | + hiera_vault_client.shutdown |
| 69 | + raise Puppet::DataBinding::LookupError, "[hiera-vault] Skipping backend. Configuration error: #{e}" |
| 70 | + end |
| 71 | + |
| 72 | + answer = nil |
| 73 | + |
| 74 | + # Only kv v2 mounts supported |
| 75 | + options['mounts'].each_pair do |mount, paths| |
| 76 | + interpolate(context, paths).each do |path| |
| 77 | + context.explain { "[hiera-vault] Looking on mount #{mount} in path #{path} for #{key}" } |
| 78 | + |
| 79 | + secret = nil |
| 80 | + |
| 81 | + begin |
| 82 | + context.explain { "[hiera-vault] Checking path: #{path} on mount: #{mount}" } |
| 83 | + secret = hiera_vault_client.kv(mount).read(File.join(path, key).chomp('/')) |
| 84 | + rescue Vault::HTTPConnectionError |
| 85 | + msg = "[hiera-vault] Could not connect to read secret: #{path} on mount: #{mount}" |
| 86 | + context.explain { msg } |
| 87 | + raise Puppet::DataBinding::LookupError, msg |
| 88 | + rescue Vault::HTTPError => e |
| 89 | + msg = "[hiera-vault] Could not read secret #{path} on mount #{mount}: #{e.errors.join("\n").rstrip}" |
| 90 | + context.explain { msg } |
| 91 | + raise Puppet::DataBinding::LookupError, msg |
| 92 | + end |
| 93 | + |
| 94 | + next if secret.nil? |
| 95 | + |
| 96 | + context.explain { "[hiera-vault] Read secret: #{key}" } |
| 97 | + # Turn secret's hash keys into strings allow for nested arrays and hashes |
| 98 | + # this enables support for create resources etc |
| 99 | + answer = secret.data.inject({}) { |h, (k, v)| h[k.to_s] = stringify_keys v; h } |
| 100 | + break |
| 101 | + end |
| 102 | + |
| 103 | + break unless answer.nil? |
| 104 | + end |
| 105 | + |
| 106 | + raise Puppet::DataBinding::LookupError, "[hiera-vault] Could not find secret #{key}" if answer.nil? |
| 107 | + |
| 108 | + answer = context.not_found if answer.nil? |
| 109 | + hiera_vault_client.shutdown |
| 110 | + |
| 111 | + return answer |
| 112 | + end |
| 113 | + |
| 114 | + # Stringify key:values so user sees expected results and nested objects |
| 115 | + def stringify_keys(value) |
| 116 | + case value |
| 117 | + when String |
| 118 | + value |
| 119 | + when Hash |
| 120 | + result = {} |
| 121 | + value.each_pair { |k, v| result[k.to_s] = stringify_keys v } |
| 122 | + result |
| 123 | + when Array |
| 124 | + value.map { |v| stringify_keys v } |
| 125 | + else |
| 126 | + value |
| 127 | + end |
| 128 | + end |
| 129 | + |
| 130 | + def interpolate(context, paths) |
| 131 | + allowed_paths = [] |
| 132 | + paths.each do |path| |
| 133 | + path = context.interpolate(path) |
| 134 | + # TODO: Unify usage of '/' - File.join seems to be a mistake, since it won't work on Windows |
| 135 | + # secret/puppet/scope1,scope2 => [[secret], [puppet], [scope1, scope2]] |
| 136 | + segments = path.split('/').map { |segment| segment.split(',') } |
| 137 | + allowed_paths += build_paths(segments) unless segments.empty? |
| 138 | + end |
| 139 | + allowed_paths |
| 140 | + end |
| 141 | + |
| 142 | + # [[secret], [puppet], [scope1, scope2]] => ['secret/puppet/scope1', 'secret/puppet/scope2'] |
| 143 | + def build_paths(segments) |
| 144 | + paths = [[]] |
| 145 | + segments.each do |segment| |
| 146 | + p = paths.dup |
| 147 | + paths.clear |
| 148 | + segment.each do |option| |
| 149 | + p.each do |path| |
| 150 | + paths << path + [option] |
| 151 | + end |
| 152 | + end |
| 153 | + end |
| 154 | + paths.map { |p| File.join(*p) } |
| 155 | + end |
| 156 | +end |
0 commit comments