foreman_vault 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dbbedb9d2f03e0e5d76b5c088d054852e1cbb8f44ff2d8454026b2fecf39215f
4
- data.tar.gz: 0f8099efcfc129acc8d640e0240ef9418b7d4a657609d13a4e2094be7c0b8ec9
3
+ metadata.gz: d4b3350992a46b726aea3cee25dbca26efcd09f9b7f2d539543d13662661b36b
4
+ data.tar.gz: 8debbd584b231a9643563801dccb9da533b5821e70f5790f1ee80dc8065fd00f
5
5
  SHA512:
6
- metadata.gz: 50d747b63207ee9ae91733eb32bbbd8ee8479e985d1acf6584928a6b1017ab73c1a753b1ed792280ae68ded7f509c01243a106f4695b5f014ae9b88e480e1291
7
- data.tar.gz: c146baed359276db93d4e696bd1b1f0cd700d022b4af12ba391cdfaccbfd858d1f0780b8d5ba20a56ea572215b19541e1213ac9b17989af05fccae6bcb0b4d76
6
+ metadata.gz: d21b6079acb7e9f4d2972f899a23d104f76c7c65ea72d367043d3a174f0b071f47f94702763b781d67ee48cdb0d20adbdf28a2f8ede9299ba210e800f1459549
7
+ data.tar.gz: 588561b8a0ccc783e14a4c2fcff7ddb66c7d4c9f2cbef90c4eb4be4c4260aacbd24230ac8da51efc34218db793ce4faa8ee5e0abf24ddbbddbb6c1fcae4913af
data/README.md CHANGED
@@ -30,6 +30,7 @@ This allows Foreman to create everything needed to access Hashicorp Vault direct
30
30
  - Foreman >= 1.20
31
31
  - Working Vault instance
32
32
  - with _cert_ auth enabled
33
+ - with _approle_ auth enabled
33
34
  - with _kv_ secret store enabled
34
35
  - valid Vault Token
35
36
 
@@ -48,6 +49,27 @@ $ vault token create -period=60m
48
49
  [...]
49
50
  ```
50
51
 
52
+ To interact with Vault you can use Vault UI, which is available at `http://127.0.0.1:8200/ui`.
53
+
54
+ - The AppRole auth method
55
+
56
+ ```
57
+ $ vault auth enable approle
58
+ $ vault write auth/approle/role/my-role policies="default"
59
+ Success! Data written to: auth/approle/role/my-role
60
+ $ vault read auth/approle/role/my-role/role-id
61
+ Key Value
62
+ --- -----
63
+ role_id 8403910c-e563-d2f2-1c77-6e26319be8b5
64
+ $ vault write -f auth/approle/role/my-role/secret-id
65
+ Key Value
66
+ --- -----
67
+ secret_id 1058434b-b4aa-bf5a-b376-a15d9efb1059
68
+ secret_id_accessor 9cc19ed7-201f-7438-782e-561edd12b2a8
69
+ ```
70
+
71
+ See also [Vault CLI testing AppRole](https://gist.github.com/kamils-iRonin/d099908eaf0500de8ad9c2cea5658d01)
72
+
51
73
  ## Installation
52
74
 
53
75
  See [Plugins install instructions](https://theforeman.org/plugins/) for how to install Foreman plugins.
@@ -9,7 +9,7 @@ module ForemanVault
9
9
  class_methods do
10
10
  def vault_connection_params_filter
11
11
  Foreman::ParameterFilter.new(::VaultConnection).tap do |filter|
12
- filter.permit :name, :url, :token
12
+ filter.permit :name, :url, :token, :role_id, :secret_id
13
13
  end
14
14
  end
15
15
  end
@@ -4,7 +4,7 @@ module ForemanVault
4
4
  module Macros
5
5
  def vault_secret(vault_connection_name, secret_path)
6
6
  vault = VaultConnection.find_by!(name: vault_connection_name)
7
- raise VaultError.new(N_('Invalid token for %s'), vault.name) unless vault.token_valid?
7
+ raise VaultError.new(N_('Invalid token for %s'), vault.name) if vault.with_token? && !vault.token_valid?
8
8
 
9
9
  vault.fetch_secret(secret_path)
10
10
  rescue ActiveRecord::RecordNotFound => e
@@ -13,7 +13,7 @@ module ForemanVault
13
13
 
14
14
  def vault_issue_certificate(vault_connection_name, secret_path, *options)
15
15
  vault = VaultConnection.find_by!(name: vault_connection_name)
16
- raise VaultError.new(N_('Invalid token for %s'), vault.name) unless vault.token_valid?
16
+ raise VaultError.new(N_('Invalid token for %s'), vault.name) if vault.with_token? && !vault.token_valid?
17
17
  vault.issue_certificate(secret_path, *options)
18
18
  rescue ActiveRecord::RecordNotFound => e
19
19
  raise VaultError, e.message
@@ -43,6 +43,7 @@ module ForemanVault
43
43
  old&.vault_auth_method&.delete
44
44
  vault_auth_method.save
45
45
  end
46
+ true
46
47
  rescue StandardError => e
47
48
  Foreman::Logging.exception("Failed to push #{name} data to Vault.", e)
48
49
  failure format(_('Failed to push %{name} data to Vault: %{message}\n '), name: name, message: e.message), e
@@ -5,20 +5,42 @@ class VaultConnection < ApplicationRecord
5
5
 
6
6
  validates_lengths_from_database
7
7
  validates :name, presence: true, uniqueness: true
8
- validates :url, :token, presence: true
8
+ validates :url, presence: true
9
9
  validates :url, format: URI.regexp(['http', 'https'])
10
10
 
11
- before_create :set_expire_time
12
- before_update :update_expire_time
11
+ validates :token, presence: true, if: -> { role_id.nil? || secret_id.nil? }
12
+ validates :token, inclusion: { in: [nil], message: _('AppRole or token must be blank') }, unless: -> { role_id.nil? || secret_id.nil? }
13
+ validates :role_id, presence: true, if: -> { token.nil? }
14
+ validates :role_id, inclusion: { in: [nil], message: _('AppRole or token must be blank') }, unless: -> { token.nil? }
15
+ validates :secret_id, presence: true, if: -> { token.nil? }
16
+ validates :secret_id, inclusion: { in: [nil], message: _('AppRole or token must be blank') }, unless: -> { token.nil? }
13
17
 
14
- scope :with_valid_token, -> { where(vault_error: nil).where('expire_time > ?', Time.zone.now) }
18
+ before_validation :normalize_blank_values
19
+ before_create :set_expire_time, unless: -> { token.nil? }
20
+ before_update :update_expire_time, unless: -> { token.nil? }
21
+
22
+ scope :with_approle, -> { where.not(role_id: nil).where.not(secret_id: nil) }
23
+ scope :with_token, -> { where.not(token: nil) }
24
+ scope :with_valid_token, -> { with_token.where(vault_error: nil).where('expire_time > ?', Time.zone.now) }
15
25
 
16
26
  delegate :fetch_expire_time, :fetch_secret, :issue_certificate,
17
27
  :policy, :policies, :put_policy, :delete_policy,
18
28
  :set_certificate, :certificates, :delete_certificate, to: :client
19
29
 
30
+ def with_token?
31
+ token.present?
32
+ end
33
+
34
+ def with_approle?
35
+ role_id.present? && secret_id.present?
36
+ end
37
+
20
38
  def token_valid?
21
- vault_error.nil? && expire_time && expire_time > Time.zone.now
39
+ return false unless with_token?
40
+ return false unless vault_error.nil?
41
+ return true unless expire_time
42
+
43
+ expire_time > Time.zone.now
22
44
  end
23
45
 
24
46
  def renew_token!
@@ -52,7 +74,13 @@ class VaultConnection < ApplicationRecord
52
74
  self.vault_error = e.message
53
75
  end
54
76
 
77
+ def normalize_blank_values
78
+ attributes.each do |column, _value|
79
+ self[column].present? || self[column] = nil
80
+ end
81
+ end
82
+
55
83
  def client
56
- @client ||= ForemanVault::VaultClient.new(url, token)
84
+ @client ||= ForemanVault::VaultClient.new(url, token, role_id, secret_id)
57
85
  end
58
86
  end
@@ -2,9 +2,11 @@
2
2
 
3
3
  module ForemanVault
4
4
  class VaultClient
5
- def initialize(base_url, token)
5
+ def initialize(base_url, token, role_id, secret_id)
6
6
  @base_url = base_url
7
7
  @token = token
8
+ @role_id = role_id
9
+ @secret_id = secret_id
8
10
  end
9
11
 
10
12
  delegate :sys, :auth_tls, to: :client
@@ -13,7 +15,8 @@ module ForemanVault
13
15
 
14
16
  def fetch_expire_time
15
17
  response = client.auth_token.lookup_self
16
- Time.zone.parse(response.data[:expire_time])
18
+ expire_time = response.data[:expire_time]
19
+ expire_time && Time.zone.parse(expire_time)
17
20
  end
18
21
 
19
22
  def fetch_secret(secret_path)
@@ -38,10 +41,16 @@ module ForemanVault
38
41
  class VaultClientError < Foreman::Exception; end
39
42
  class NoDataError < VaultClientError; end
40
43
 
41
- attr_reader :base_url, :token
44
+ attr_reader :base_url, :token, :role_id, :secret_id
42
45
 
43
46
  def client
44
- @client ||= Vault::Client.new(address: base_url, token: token)
47
+ @client ||= if role_id.present? && secret_id.present?
48
+ Vault::Client.new(address: base_url).tap do |client|
49
+ client.auth.approle(role_id, secret_id)
50
+ end
51
+ else
52
+ Vault::Client.new(address: base_url, token: token)
53
+ end
45
54
  end
46
55
  end
47
56
  end
@@ -1,7 +1,23 @@
1
- <%= form_for @vault_connection, :url => (@vault_connection.new_record? ? vault_connections_path : vault_connection_path(:id => @vault_connection)) do |f| %>
1
+ <%= form_for @vault_connection, url: (@vault_connection.new_record? ? vault_connections_path : vault_connection_path(id: @vault_connection)) do |f| %>
2
2
  <%= base_errors_for @vault_connection %>
3
- <%= text_f f, :name, :help_inline => _("Vault Connection name") %>
4
- <%= text_f f, :url, :help_inline => _("Vault Server url") %>
5
- <%= text_f f, :token, :help_inline => _("Vault Connection token") %>
3
+ <%= text_f f, :name, help_inline: _("Vault Connection name") %>
4
+ <%= text_f f, :url, help_inline: _("Vault Server url") %>
5
+ <div class="auth_methods">
6
+ <h4><%=_("Auth Methods")%></h4>
7
+ <%= alert(text: _('Please only fill in one auth method'), class: 'alert-info', close: false) %>
8
+ <ul class="nav nav-tabs" data-tabs="tabs">
9
+ <li class="active"><a href="#approle" data-toggle="tab"><%= _('AppRole') %></a></li>
10
+ <li><a href="#token" data-toggle="tab"><%= _('Token') %></a></li>
11
+ </ul>
12
+ <div class="tab-content">
13
+ <div class="tab-pane active" id="approle">
14
+ <%= text_f f, :role_id, label: _("Role ID"), help_inline: _("Vault Connection Role ID") %>
15
+ <%= text_f f, :secret_id, label: _("Secret ID"), help_inline: _("Vault Connection Secret ID") %>
16
+ </div>
17
+ <div class="tab-pane" id="token">
18
+ <%= text_f f, :token, help_inline: _("Vault Connection token") %>
19
+ </div>
20
+ </div>
21
+ </div>
6
22
  <%= submit_or_cancel f %>
7
23
  <% end %>
@@ -4,9 +4,11 @@
4
4
  <table class="<%= table_css_classes 'table-fixed' %>">
5
5
  <thead>
6
6
  <tr>
7
- <th class="col-md-9"><%= sort :name, :as => s_('Vault Connections|Name') %></th>
8
- <th class="col-md-1"><%= _('Valid') %></th>
9
- <th class="col-md-1"><%= _('Expire time') %></th>
7
+ <th class="col-md-2"><%= sort :name, as: s_('Vault Connections|Name') %></th>
8
+ <th class="col-md-2"><%= _('URL') %></th>
9
+ <th class="col-md-2"><%= _('Role ID') %></th>
10
+ <th class="col-md-1"><%= _('Token Valid') %></th>
11
+ <th class="col-md-1"><%= _('Token Expire time') %></th>
10
12
  <th class="col-md-1"><%= _('Actions') %></th>
11
13
  </tr>
12
14
  </thead>
@@ -14,17 +16,29 @@
14
16
  <% @vault_connections.each do |vault_connection| %>
15
17
  <tr>
16
18
  <td class="ellipsis">
17
- <%= link_to_if_authorized vault_connection.name, hash_for_edit_vault_connection_path(:id => vault_connection) %>
19
+ <%= link_to_if_authorized vault_connection.name, hash_for_edit_vault_connection_path(id: vault_connection) %>
20
+ </td>
21
+ <td class="ellipsis">
22
+ <code class="transparent"><%= vault_connection.url %></code>
23
+ </td>
24
+ <td class="ellipsis">
25
+ <% if vault_connection.with_approle? %>
26
+ <code class="transparent"><%= vault_connection.role_id %></code>
27
+ <% end %>
18
28
  </td>
19
29
  <td align='center'>
20
- <% if vault_connection.token_valid? %>
30
+ <% vault_connection.with_token? && if vault_connection.token_valid? %>
21
31
  <%= ('<span class="glyphicon glyphicon-ok"/>').html_safe %>
22
32
  <% else %>
23
33
  <%= ('<span class="glyphicon glyphicon-remove" title="%s"/>' % vault_connection.vault_error).html_safe %>
24
34
  <% end %>
25
35
  </td>
26
- <td><%= date_time_absolute(vault_connection.expire_time) %></td>
27
- <td><%= action_buttons display_delete_if_authorized hash_for_vault_connection_path(:id => vault_connection), :data => { :confirm => _("Delete %s?") % vault_connection.name } %></td>
36
+ <td>
37
+ <% if vault_connection.with_token? %>
38
+ <%= date_time_absolute(vault_connection.expire_time) %>
39
+ <% end %>
40
+ </td>
41
+ <td><%= action_buttons display_delete_if_authorized hash_for_vault_connection_path(id: vault_connection), data: { confirm: _("Delete %s?") % vault_connection.name } %></td>
28
42
  </tr>
29
43
  <% end %>
30
44
  </tbody>
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddApproleToVaultConnection < ActiveRecord::Migration[5.1]
4
+ def change
5
+ add_column :vault_connections, :role_id, :string
6
+ add_column :vault_connections, :secret_id, :string
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanVault
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -8,7 +8,7 @@ FactoryBot.define do
8
8
  expire_time { Time.zone.now + 1.year }
9
9
 
10
10
  trait :invalid do
11
- expire_time { nil }
11
+ expire_time { Time.zone.now - 1.year }
12
12
  end
13
13
 
14
14
  trait :without_callbacks do
@@ -3,10 +3,46 @@
3
3
  require 'test_plugin_helper'
4
4
 
5
5
  class VaultClientTest < ActiveSupport::TestCase
6
- setup do
7
- @subject = ForemanVault::VaultClient.new('http://127.0.0.1:8200', 'e57e5ef2-b25c-a0e5-65a6-863ab095dff6')
8
- @client = mock
9
- Vault::Client.expects(:new).returns(@client)
6
+ subject do
7
+ ForemanVault::VaultClient.new(base_url, token, nil, nil).tap do |vault_client|
8
+ vault_client.instance_variable_set(:@client, client)
9
+ end
10
+ end
11
+
12
+ let(:client) { Vault::Client }
13
+ let(:base_url) { 'http://127.0.0.1:8200' }
14
+ let(:token) { 's.opkr0MAqme5e5nr3i2or5wZC' }
15
+
16
+ describe 'auth with AppRole' do
17
+ subject { ForemanVault::VaultClient.new(base_url, nil, role_id, secret_id) }
18
+
19
+ let(:role_id) { '8403910c-e563-d2f2-1c77-6e26319be8b5' }
20
+ let(:secret_id) { '1058434b-b4aa-bf5a-b376-a15d9efb1059' }
21
+
22
+ setup do
23
+ stub_request(:post, "#{base_url}/v1/auth/approle/login").with(
24
+ body: {
25
+ role_id: role_id,
26
+ secret_id: secret_id
27
+ }
28
+ ).to_return(
29
+ status: 200,
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: {
32
+ auth: {
33
+ client_token: token
34
+ }
35
+ }.to_json
36
+ )
37
+ end
38
+
39
+ it { assert_equal token, subject.send(:client).token }
40
+ end
41
+
42
+ describe 'auth with token' do
43
+ subject { ForemanVault::VaultClient.new(base_url, token, nil, nil) }
44
+
45
+ it { assert_equal token, subject.send(:client).token }
10
46
  end
11
47
 
12
48
  describe '#fetch_expire_time' do
@@ -14,11 +50,11 @@ class VaultClientTest < ActiveSupport::TestCase
14
50
  @time = '2018-08-01T20:08:55.525830559+02:00'
15
51
  response = OpenStruct.new(data: { expire_time: @time })
16
52
  auth_token = mock.tap { |object| object.expects(:lookup_self).once.returns(response) }
17
- @client.expects(:auth_token).once.returns(auth_token)
53
+ client.expects(:auth_token).once.returns(auth_token)
18
54
  end
19
55
 
20
56
  test 'should return expire time' do
21
- assert_equal Time.zone.parse(@time), @subject.fetch_expire_time
57
+ assert_equal Time.zone.parse(@time), subject.fetch_expire_time
22
58
  end
23
59
  end
24
60
 
@@ -29,11 +65,11 @@ class VaultClientTest < ActiveSupport::TestCase
29
65
  response = OpenStruct.new(data: @data)
30
66
  logical = mock.tap { |object| object.expects(:read).once.with(@secret_path).returns(response) }
31
67
 
32
- @client.expects(:logical).once.returns(logical)
68
+ client.expects(:logical).once.returns(logical)
33
69
  end
34
70
 
35
71
  test 'should return expire time' do
36
- assert_equal @data, @subject.fetch_secret(@secret_path)
72
+ assert_equal @data, subject.fetch_secret(@secret_path)
37
73
  end
38
74
  end
39
75
 
@@ -52,11 +88,11 @@ class VaultClientTest < ActiveSupport::TestCase
52
88
  response = OpenStruct.new(data: @data)
53
89
  logical = mock.tap { |object| object.expects(:write).once.with(@pki_path).returns(response) }
54
90
 
55
- @client.expects(:logical).once.returns(logical)
91
+ client.expects(:logical).once.returns(logical)
56
92
  end
57
93
 
58
94
  test 'should return new certificate' do
59
- assert_equal @data, @subject.issue_certificate(@pki_path)
95
+ assert_equal @data, subject.issue_certificate(@pki_path)
60
96
  end
61
97
  end
62
98
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_vault
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dmTECH GmbH
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-10 00:00:00.000000000 Z
11
+ date: 2021-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: vault
@@ -92,6 +92,7 @@ files:
92
92
  - config/routes.rb
93
93
  - db/migrate/20180725072913_create_vault_connection.foreman_vault.rb
94
94
  - db/migrate/20180809172407_rename_vault_status_to_vault_error.foreman_vault.rb
95
+ - db/migrate/20201203220058_add_approle_to_vault_connection.rb
95
96
  - db/seeds.d/103-provisioning_templates.rb
96
97
  - lib/foreman_vault.rb
97
98
  - lib/foreman_vault/engine.rb
@@ -135,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
136
  - !ruby/object:Gem::Version
136
137
  version: '0'
137
138
  requirements: []
138
- rubygems_version: 3.1.2
139
+ rubygems_version: 3.1.4
139
140
  signing_key:
140
141
  specification_version: 4
141
142
  summary: Adds support for using credentials from Hashicorp Vault