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

Add lookup_key function to get at a specific key value #21

Closed
wants to merge 4 commits into from
Closed
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
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ You can also choose not to specify the Vault URL, and then Puppet will use the
set in the service config file for Puppet, on Debian `/etc/default/puppet`, on RedHat
`/etc/sysconfig/puppet`:

```
```puppet
$d = Deferred('vault_lookup::lookup', ["secret/test"])

node default {
Expand All @@ -113,3 +113,46 @@ node default {
}
}
```

The standard lookup call will return a hash of the keys found at the path you
specified. This can occasionally be difficult to manipulate within your Puppet code.
If you wish to look up a specific key at a path and get back the value as a string, you can
use the `lookup_key` function instead. An example follows:

```puppet
$d = Deferred('vault_lookup::lookup_key', ["secret/test", 'mykey', 'https://vault.hostname:8200'])

node default {
notify { example :
message => $d
}
}
```

Aside from requiring a key to be passed as the second argument and returning a string, it behaves
exactly the same as `lookup`. (including making use of the `VAULT_ADDR` environment variable
if you don't specify a Vault URL)

If you are attempting to use Deferred calls to retrieve data that goes into another string, or a
a number of other Puppet functions, for example:

```puppet
$d = Deferred('vault_lookup::lookup_key', ["secret/test", 'mykey', 'https://vault.hostname:8200'])

node default {
notify { example :
message => "key is set to ${d}"
}
}
```

You will find that the message that comes out looks something like:

```output
key is set to Deferred({'name' => 'lookup_key', 'arguments' => [.........]})
```

You can work around that by using `${d.call}`, but that does result in the call being processed
during compilation (on the Puppet master) instead of on the agent, which you may not want.

You can quite a few tips for working with `Deferred` [here](http://puppet-on-the-edge.blogspot.com/2018/10/the-topic-is-deferred.html).
73 changes: 73 additions & 0 deletions lib/puppet/functions/vault_lookup/lookup_key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
Puppet::Functions.create_function(:'vault_lookup::lookup_key') do
dispatch :lookup_key do
param 'String', :path
param 'String', :key
optional_param 'String', :vault_url
end

def lookup_key(path, key, vault_url = nil)
if vault_url.nil?
Puppet.debug 'No Vault address was set on function, defaulting to value from VAULT_ADDR env value'
vault_url = ENV['VAULT_ADDR']
raise Puppet::Error, 'No vault_url given and VAULT_ADDR env variable not set' if vault_url.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to also check for an empty string here, not just nil?

end

uri = URI(vault_url)
# URI is used here to just parse the vault_url into a host string
# and port; it's possible to generate a URI::Generic when a scheme
# is not defined, so double check here to make sure at least
# host is defined.
raise Puppet::Error, "Unable to parse a hostname from #{vault_url}" unless uri.hostname
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless this accounts for my above concerns with an empty string.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does in that it freaks out if it's not a valid url (and an empty string qualifies) -- it's not really the cleanest implementation. That said it mirrors what is (or was if it's changed) in lookup.rb =) I somewhat wonder if this would be better served as an additional optional argument to lookup itself. However it was a quick solution and I'm using it in production so it's got that goin' for it. =D

Going to add another comment related to this kinda.


use_ssl = uri.scheme == 'https'
connection = Puppet::Network::HttpPool.http_instance(uri.host, uri.port, use_ssl)

token = get_auth_token(connection)

secret_response = connection.get("/v1/#{path}", 'X-Vault-Token' => token)
unless secret_response.is_a?(Net::HTTPOK)
message = "Received #{secret_response.code} response code from vault at #{uri.host} for secret lookup"
raise Puppet::Error, append_api_errors(message, secret_response)
end

begin
data = JSON.parse(secret_response.body)['data']
rescue StandardError
raise Puppet::Error, 'Error parsing json secret data from vault response'
end

data_from_key = data[key]
Puppet::Pops::Types::PSensitiveType::Sensitive.new(data_from_key)
end

private

def get_auth_token(connection)
response = connection.post('/v1/auth/cert/login', '')
unless response.is_a?(Net::HTTPOK)
message = "Received #{response.code} response code from vault at #{connection.address} for authentication"
raise Puppet::Error, append_api_errors(message, response)
end

begin
token = JSON.parse(response.body)['auth']['client_token']
rescue StandardError
raise Puppet::Error, 'Unable to parse client_token from vault response'
end

raise Puppet::Error, 'No client_token found' if token.nil?

token
end

def append_api_errors(message, response)
errors = begin
JSON.parse(response.body)['errors']
rescue StandardError
nil
end
message << " (api errors: #{errors})" if errors

message
end
end
141 changes: 141 additions & 0 deletions spec/functions/lookup_key_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
require 'spec_helper'

describe 'vault_lookup::lookup_key' do
let(:function) { subject }

let(:auth_success_data) do
<<~JSON
{
"request_id": "03d11bd4-b994-c432-150f-5703a75641d1",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "7dad29d2-40af-038f-cf9c-0aeb616f8d20",
"accessor": "fd0c3269-9642-25e5-cebe-27a732be53a0",
"policies": [
"default",
"secret_reader"
],
"token_policies": [
"default",
"secret_reader"
],
"metadata": {
"authority_key_id": "b7:da:18:2f:cc:09:18:5d:d0:c5:24:0a:0a:66:46:ba:0d:f0:ea:4a",
"cert_name": "vault.docker",
"common_name": "localhost",
"serial_number": "5",
"subject_key_id": "ea:00:c0:0b:2d:38:01:28:ba:16:1f:08:64:de:0a:7c:8f:b7:43:33"
},
"lease_duration": 604800,
"renewable": true,
"entity_id": "e1bc06c5-303e-eec7-bf58-2a74fae2ec3d"
}
}
JSON
end

let(:auth_failure_data) do
'{"errors":["invalid certificate or no client certificate supplied"]}'
end

let(:secret_success_data) do
'{"request_id":"e394e8ef-78f3-ac85-fbeb-33f060e911d4","lease_id":"","renewable":false,"lease_duration":604800,"data":{"foo":"bar"},"wrap_info":null,"warnings":null,"auth":null}
'
end

let(:permission_denied_data) do
'{"errors":["permission denied"]}'
end

it 'errors for malformed uri' do
expect {
function.execute('/v1/whatever', 'foo', 'vault.docker')
}.to raise_error(Puppet::Error, %r{Unable to parse a hostname})
end

it 'errors when no vault_url set and no VAULT_ADDR environment variable' do
expect {
function.execute('/v1/whatever', 'foo')
}.to raise_error(Puppet::Error, %r{No vault_url given and VAULT_ADDR env variable not set})
end

it 'raises a Puppet error when auth fails' do
connection = instance_double('Puppet::Network::HTTP::Connection', address: 'vault.doesnotexist')
expect(Puppet::Network::HttpPool).to receive(:http_instance).and_return(connection)

response = Net::HTTPForbidden.new('1.1', 403, auth_failure_data)
allow(response).to receive(:body).and_return(auth_failure_data)
expect(connection).to receive(:post).with('/v1/auth/cert/login', '').and_return(response)

expect {
function.execute('thepath', 'thekey', 'https://vault.doesnotexist:8200')
}.to raise_error(Puppet::Error, %r{Received 403 response code from vault.*invalid certificate or no client certificate supplied})
end

it 'raises a Puppet error when data lookup fails' do
connection = instance_double('Puppet::Network::HTTP::Connection', address: 'vault.doesnotexist')
expect(Puppet::Network::HttpPool).to receive(:http_instance).and_return(connection)

auth_response = Net::HTTPOK.new('1.1', 200, '')
expect(auth_response).to receive(:body).and_return(auth_success_data)
expect(connection).to receive(:post).with('/v1/auth/cert/login', '').and_return(auth_response)

secret_response = Net::HTTPForbidden.new('1.1', 403, permission_denied_data)
allow(secret_response).to receive(:body).and_return(permission_denied_data)
expect(connection)
.to receive(:get)
.with('/v1/secret/test', hash_including('X-Vault-Token' => '7dad29d2-40af-038f-cf9c-0aeb616f8d20'))
.and_return(secret_response)

expect {
function.execute('secret/test', 'foo', 'https://vault.doesnotexist:8200')
}.to raise_error(Puppet::Error, %r{Received 403 response code from vault at vault.doesnotexist for secret lookup.*permission denied})
end

it 'logs on, requests a key of a secret using a token, and returns the data value wrapped in the Sensitive type' do
connection = instance_double('Puppet::Network::HTTP::Connection', address: 'vault.doesnotexist')
expect(Puppet::Network::HttpPool).to receive(:http_instance).and_return(connection)

auth_response = Net::HTTPOK.new('1.1', 200, '')
expect(auth_response).to receive(:body).and_return(auth_success_data)
expect(connection).to receive(:post).with('/v1/auth/cert/login', '').and_return(auth_response)

secret_response = Net::HTTPOK.new('1.1', 200, '')
expect(secret_response).to receive(:body).and_return(secret_success_data)
expect(connection)
.to receive(:get)
.with('/v1/secret/test', hash_including('X-Vault-Token' => '7dad29d2-40af-038f-cf9c-0aeb616f8d20'))
.and_return(secret_response)

result = function.execute('secret/test', 'foo', 'https://vault.doesnotexist:8200')
expect(result).to be_a(Puppet::Pops::Types::PSensitiveType::Sensitive)
expect(result.unwrap).to eq('bar')
end

it 'logs on, requests a key of a secret using a token, and returns the data value wrapped in the Sensitive type from VAULT_ADDR' do
stub_const('ENV', ENV.to_hash.merge('VAULT_ADDR' => 'https://vaultenv.doesnotexist:8200'))

connection = instance_double('Puppet::Network::HTTP::Connection', address: 'vaultenv.doesnotexist:8200')
expect(Puppet::Network::HttpPool).to receive(:http_instance).with('vaultenv.doesnotexist', 8200, true).and_return(connection)

auth_response = Net::HTTPOK.new('1.1', 200, '')
expect(auth_response).to receive(:body).and_return(auth_success_data)
expect(connection).to receive(:post).with('/v1/auth/cert/login', '').and_return(auth_response)

secret_response = Net::HTTPOK.new('1.1', 200, '')
expect(secret_response).to receive(:body).and_return(secret_success_data)
expect(connection)
.to receive(:get)
.with('/v1/secret/test', hash_including('X-Vault-Token' => '7dad29d2-40af-038f-cf9c-0aeb616f8d20'))
.and_return(secret_response)

result = function.execute('secret/test', 'foo')
expect(result).to be_a(Puppet::Pops::Types::PSensitiveType::Sensitive)
expect(result.unwrap).to eq('bar')
end
end