-
Notifications
You must be signed in to change notification settings - Fork 43
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
Changes from all commits
2a63dff
fc9a56e
0e8883c
bdd6f92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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? | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless this accounts for my above concerns with an empty string. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 |
There was a problem hiding this comment.
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?