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 +4 -4
- data/README.md +22 -0
- data/app/controllers/concerns/foreman_vault/controller/parameters/vault_connection.rb +1 -1
- data/app/lib/foreman_vault/macros.rb +2 -2
- data/app/models/concerns/foreman_vault/orchestration/vault_policy.rb +1 -0
- data/app/models/vault_connection.rb +34 -6
- data/app/services/foreman_vault/vault_client.rb +13 -4
- data/app/views/vault_connections/_form.html.erb +20 -4
- data/app/views/vault_connections/index.html.erb +21 -7
- data/db/migrate/20201203220058_add_approle_to_vault_connection.rb +8 -0
- data/lib/foreman_vault/version.rb +1 -1
- data/test/factories/vault_connection.rb +1 -1
- data/test/unit/services/foreman_vault/vault_client_test.rb +46 -10
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d4b3350992a46b726aea3cee25dbca26efcd09f9b7f2d539543d13662661b36b
|
4
|
+
data.tar.gz: 8debbd584b231a9643563801dccb9da533b5821e70f5790f1ee80dc8065fd00f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
@@ -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)
|
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)
|
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,
|
8
|
+
validates :url, presence: true
|
9
9
|
validates :url, format: URI.regexp(['http', 'https'])
|
10
10
|
|
11
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 ||=
|
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, :
|
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, :
|
4
|
-
<%= text_f f, :url, :
|
5
|
-
|
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-
|
8
|
-
<th class="col-md-
|
9
|
-
<th class="col-md-
|
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(:
|
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
|
27
|
-
|
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>
|
@@ -3,10 +3,46 @@
|
|
3
3
|
require 'test_plugin_helper'
|
4
4
|
|
5
5
|
class VaultClientTest < ActiveSupport::TestCase
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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),
|
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
|
-
|
68
|
+
client.expects(:logical).once.returns(logical)
|
33
69
|
end
|
34
70
|
|
35
71
|
test 'should return expire time' do
|
36
|
-
assert_equal @data,
|
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
|
-
|
91
|
+
client.expects(:logical).once.returns(logical)
|
56
92
|
end
|
57
93
|
|
58
94
|
test 'should return new certificate' do
|
59
|
-
assert_equal @data,
|
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.
|
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:
|
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.
|
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
|