foreman_vault 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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