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 +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
|