foreman_vault 0.0.1 → 1.0.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 +115 -10
- data/app/controllers/concerns/foreman_vault/controller/parameters/vault_connection.rb +1 -1
- data/app/lib/foreman_vault/macros.rb +10 -1
- data/app/models/concerns/foreman_vault/host_extensions.rb +31 -0
- data/app/models/concerns/foreman_vault/orchestration/vault_policy.rb +70 -0
- data/app/models/concerns/foreman_vault/provisioning_template_extensions.rb +15 -0
- data/app/models/setting/vault.rb +85 -0
- data/app/models/vault_connection.rb +38 -7
- data/app/services/foreman_vault/vault_auth_method.rb +58 -0
- data/app/services/foreman_vault/vault_client.rb +24 -6
- data/app/services/foreman_vault/vault_policy.rb +65 -0
- data/app/views/unattended/provisioning_templates/VaultPolicy/default.erb +6 -0
- data/app/views/vault_connections/_form.html.erb +20 -4
- data/app/views/vault_connections/index.html.erb +21 -7
- data/app/views/vault_connections/welcome.html.erb +12 -0
- data/db/migrate/20201203220058_add_approle_to_vault_connection.rb +8 -0
- data/db/seeds.d/103-provisioning_templates.rb +25 -0
- data/lib/foreman_vault/engine.rb +15 -3
- data/lib/foreman_vault/version.rb +1 -1
- data/test/factories/{foreman_vault_factories.rb → vault_connection.rb} +3 -3
- data/test/factories/vault_policy_template.rb +11 -0
- data/test/factories/vault_setting.rb +10 -0
- data/test/fixtures/ca.crt +21 -0
- data/test/functional/api/v2/vault_connections_controller_test.rb +1 -1
- data/test/models/foreman_vault/orchestration/vault_policy_test.rb +167 -0
- data/test/models/vault_policy_template_test.rb +28 -0
- data/test/test_plugin_helper.rb +0 -1
- data/test/unit/services/foreman_vault/vault_auth_method_test.rb +130 -0
- data/test/unit/services/foreman_vault/vault_client_test.rb +67 -8
- data/test/unit/services/foreman_vault/vault_policy_test.rb +171 -0
- metadata +33 -10
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanVault
|
4
|
+
class VaultAuthMethod
|
5
|
+
def initialize(host)
|
6
|
+
@host = host
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid?
|
10
|
+
name.present? && options[:certificate].present?
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
return if !host || !vault_policy_name
|
15
|
+
|
16
|
+
[host, vault_policy_name].join('-').parameterize
|
17
|
+
end
|
18
|
+
|
19
|
+
def save
|
20
|
+
return false unless valid?
|
21
|
+
|
22
|
+
set_certificate(name, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete
|
26
|
+
return false unless valid?
|
27
|
+
|
28
|
+
delete_certificate(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :host
|
34
|
+
delegate :vault_policy, :vault_connection, :fqdn, to: :host
|
35
|
+
delegate :name, to: :vault_policy, prefix: true
|
36
|
+
delegate :set_certificate, :delete_certificate, to: :vault_connection
|
37
|
+
|
38
|
+
def options
|
39
|
+
{
|
40
|
+
certificate: certificate,
|
41
|
+
token_policies: vault_policy_name,
|
42
|
+
allowed_common_names: allowed_common_names
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def allowed_common_names
|
47
|
+
[fqdn].compact
|
48
|
+
end
|
49
|
+
|
50
|
+
def certificate
|
51
|
+
return unless Setting['ssl_ca_file']
|
52
|
+
|
53
|
+
File.read(Setting['ssl_ca_file'])
|
54
|
+
rescue Errno::ENOENT
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -1,22 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'vault'
|
4
|
-
|
5
3
|
module ForemanVault
|
6
4
|
class VaultClient
|
7
|
-
def initialize(base_url, token)
|
5
|
+
def initialize(base_url, token, role_id, secret_id)
|
8
6
|
@base_url = base_url
|
9
7
|
@token = token
|
8
|
+
@role_id = role_id
|
9
|
+
@secret_id = secret_id
|
10
10
|
end
|
11
11
|
|
12
|
+
delegate :sys, :auth_tls, to: :client
|
13
|
+
delegate :policy, :policies, :put_policy, :delete_policy, to: :sys
|
14
|
+
delegate :certificate, :certificates, :set_certificate, :delete_certificate, to: :auth_tls
|
15
|
+
|
12
16
|
def fetch_expire_time
|
13
17
|
response = client.auth_token.lookup_self
|
14
|
-
|
18
|
+
expire_time = response.data[:expire_time]
|
19
|
+
expire_time && Time.zone.parse(expire_time)
|
15
20
|
end
|
16
21
|
|
17
22
|
def fetch_secret(secret_path)
|
18
23
|
response = client.logical.read(secret_path)
|
19
24
|
raise NoDataError.new(N_('There is no available data for path: %s'), secret_path) unless response
|
25
|
+
|
26
|
+
response.data
|
27
|
+
end
|
28
|
+
|
29
|
+
def issue_certificate(secret_path, *options)
|
30
|
+
response = client.logical.write(secret_path, *options)
|
31
|
+
raise NoDataError.new(N_('Could not issue certificate: %s'), secret_path) unless response
|
20
32
|
response.data
|
21
33
|
end
|
22
34
|
|
@@ -29,10 +41,16 @@ module ForemanVault
|
|
29
41
|
class VaultClientError < Foreman::Exception; end
|
30
42
|
class NoDataError < VaultClientError; end
|
31
43
|
|
32
|
-
attr_reader :base_url, :token
|
44
|
+
attr_reader :base_url, :token, :role_id, :secret_id
|
33
45
|
|
34
46
|
def client
|
35
|
-
@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
|
36
54
|
end
|
37
55
|
end
|
38
56
|
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ForemanVault
|
4
|
+
class VaultPolicy
|
5
|
+
MAGIC_COMMENT_NAME_PREFIX = '# NAME: '
|
6
|
+
|
7
|
+
def initialize(host)
|
8
|
+
@host = host
|
9
|
+
end
|
10
|
+
|
11
|
+
def valid?
|
12
|
+
name.present? && rules.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
magic_comment_name&.chomp&.remove(MAGIC_COMMENT_NAME_PREFIX)&.parameterize
|
17
|
+
end
|
18
|
+
|
19
|
+
def new?
|
20
|
+
return unless name
|
21
|
+
|
22
|
+
policies.index(name).nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def save
|
26
|
+
return false unless valid?
|
27
|
+
|
28
|
+
put_policy(name, rules)
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete
|
32
|
+
return false unless valid?
|
33
|
+
|
34
|
+
delete_policy(name)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :host
|
40
|
+
delegate :params, :render_template, :vault_connection, to: :host
|
41
|
+
delegate :policy, :policies, :put_policy, :delete_policy, to: :vault_connection
|
42
|
+
|
43
|
+
def rules
|
44
|
+
rendered&.remove(magic_comment_name)
|
45
|
+
&.lines
|
46
|
+
&.reject { |l| l.strip.empty? }
|
47
|
+
&.join
|
48
|
+
&.presence
|
49
|
+
end
|
50
|
+
|
51
|
+
def magic_comment_name
|
52
|
+
rendered&.lines&.find { |l| l.start_with?(MAGIC_COMMENT_NAME_PREFIX) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def rendered
|
56
|
+
return unless template
|
57
|
+
|
58
|
+
render_template(template: template)
|
59
|
+
end
|
60
|
+
|
61
|
+
def template
|
62
|
+
::ProvisioningTemplate.find_by(name: Setting['vault_policy_template'])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
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>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<div class="blank-slate-pf">
|
2
|
+
<div class="blank-slate-pf-icon">
|
3
|
+
<%= icon_text("shield", "", :kind => "fa") %>
|
4
|
+
</div>
|
5
|
+
<h1><%= _('Vault Connections') %></h1>
|
6
|
+
<p>
|
7
|
+
<%= _("HashiCorp Vault is a secrets management solution that brokers access for both humans and machines, through programmatic access, to systems. Secrets can be stored, dynamically generated, and in the case of encryption, keys can be consumed as a service without the need to expose the underlying key materials.") %></br>
|
8
|
+
</p>
|
9
|
+
<div class="blank-slate-pf-main-action">
|
10
|
+
<%= new_link(_("New Vault Connection"), {}, { :class => "btn-lg" }) %>
|
11
|
+
</div>
|
12
|
+
</div>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
User.as_anonymous_admin do
|
4
|
+
templates = [
|
5
|
+
{
|
6
|
+
name: 'Default Vault Policy',
|
7
|
+
source: 'VaultPolicy/default.erb',
|
8
|
+
template_kind: TemplateKind.find_or_create_by(name: 'VaultPolicy')
|
9
|
+
}
|
10
|
+
]
|
11
|
+
|
12
|
+
templates.each do |template|
|
13
|
+
template[:contents] = File.read(File.join(ForemanVault::Engine.root, 'app/views/unattended/provisioning_templates', template[:source]))
|
14
|
+
|
15
|
+
ProvisioningTemplate.where(name: template[:name]).first_or_create do |pt|
|
16
|
+
pt.vendor = 'ForemanVault'
|
17
|
+
pt.default = true
|
18
|
+
pt.locked = true
|
19
|
+
pt.name = template[:name]
|
20
|
+
pt.template = template[:contents]
|
21
|
+
pt.template_kind = template[:template_kind] if template[:template_kind]
|
22
|
+
pt.snippet = template[:snippet] if template[:snippet]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/foreman_vault/engine.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'vault'
|
4
|
+
|
3
5
|
module ForemanVault
|
4
6
|
class Engine < ::Rails::Engine
|
5
7
|
engine_name 'foreman_vault'
|
@@ -10,6 +12,14 @@ module ForemanVault
|
|
10
12
|
config.autoload_paths += Dir["#{config.root}/app/lib"]
|
11
13
|
config.autoload_paths += Dir["#{config.root}/app/jobs"]
|
12
14
|
|
15
|
+
initializer 'foreman_vault.load_default_settings', before: :load_config_initializers do
|
16
|
+
require_dependency File.expand_path('../../app/models/setting/vault.rb', __dir__) if begin
|
17
|
+
Setting.table_exists?
|
18
|
+
rescue StandardError
|
19
|
+
(false)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
13
23
|
# Add any db migrations
|
14
24
|
initializer 'foreman_vault.load_app_instance_data' do |app|
|
15
25
|
ForemanVault::Engine.paths['db/migrate'].existent.each do |path|
|
@@ -19,7 +29,7 @@ module ForemanVault
|
|
19
29
|
|
20
30
|
initializer 'foreman_vault.register_plugin', before: :finisher_hook do |_app|
|
21
31
|
Foreman::Plugin.register :foreman_vault do
|
22
|
-
requires_foreman '>=
|
32
|
+
requires_foreman '>= 2.3'
|
23
33
|
|
24
34
|
apipie_documented_controllers ["#{ForemanVault::Engine.root}/app/controllers/api/v2/*.rb"]
|
25
35
|
|
@@ -44,8 +54,10 @@ module ForemanVault
|
|
44
54
|
|
45
55
|
config.to_prepare do
|
46
56
|
begin
|
47
|
-
|
48
|
-
|
57
|
+
::Host::Managed.include(ForemanVault::HostExtensions)
|
58
|
+
::ProvisioningTemplate.include(ForemanVault::ProvisioningTemplateExtensions)
|
59
|
+
::Foreman::Renderer::Scope::Base.include(ForemanVault::Macros)
|
60
|
+
::Foreman::Renderer.configure { |c| c.allowed_generic_helpers += [:vault_secret, :vault_issue_certificate] }
|
49
61
|
rescue StandardError => e
|
50
62
|
Rails.logger.warn "ForemanVault: skipping engine hook (#{e})"
|
51
63
|
end
|
@@ -3,12 +3,12 @@
|
|
3
3
|
FactoryBot.define do
|
4
4
|
factory :vault_connection, class: VaultConnection do
|
5
5
|
sequence(:name) { |n| "VaultServer-#{n}" }
|
6
|
-
url 'http://localhost:8200'
|
7
|
-
token '16aa4f29-035d-b604-f3d3-8cd9a6a6921c'
|
6
|
+
url { 'http://localhost:8200' }
|
7
|
+
token { '16aa4f29-035d-b604-f3d3-8cd9a6a6921c' }
|
8
8
|
expire_time { Time.zone.now + 1.year }
|
9
9
|
|
10
10
|
trait :invalid do
|
11
|
-
expire_time
|
11
|
+
expire_time { Time.zone.now - 1.year }
|
12
12
|
end
|
13
13
|
|
14
14
|
trait :without_callbacks do
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
FactoryBot.modify do
|
4
|
+
factory :provisioning_template do
|
5
|
+
trait :vault_policy do
|
6
|
+
name { Setting['vault_policy_template'] || 'Default Vault Policy' }
|
7
|
+
template_kind { TemplateKind.find_or_create_by(name: 'VaultPolicy') }
|
8
|
+
template { File.read(File.join(ForemanVault::Engine.root, 'app/views/unattended/provisioning_templates/VaultPolicy/default.erb')) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIDgDCCAmgCCQDSQhCJzHnXejANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC
|
3
|
+
cGwxDjAMBgNVBAgMBXN0YXRlMQ0wCwYDVQQHDARjaXR5MRAwDgYDVQQKDAdjb21w
|
4
|
+
YW55MRAwDgYDVQQLDAdzZWN0aW9uMQ0wCwYDVQQDDARob3N0MSAwHgYJKoZIhvcN
|
5
|
+
AQkBFhFlbWFpbEBleGFtcGxlLmNvbTAeFw0yMDA0MjkxMzQ1MjdaFw0yMzAyMTcx
|
6
|
+
MzQ1MjdaMIGBMQswCQYDVQQGEwJwbDEOMAwGA1UECAwFc3RhdGUxDTALBgNVBAcM
|
7
|
+
BGNpdHkxEDAOBgNVBAoMB2NvbXBhbnkxEDAOBgNVBAsMB3NlY3Rpb24xDTALBgNV
|
8
|
+
BAMMBGhvc3QxIDAeBgkqhkiG9w0BCQEWEWVtYWlsQGV4YW1wbGUuY29tMIIBIjAN
|
9
|
+
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzQxWM6dJbtdDrIUvG5SBp8XadSst
|
10
|
+
D5Lu/WBwazdiRQI6mF6bOrAviZUJVoN0sFphZHjQpzTkxS9N929fdChBB7fjrP7b
|
11
|
+
vydvcbc8eH+HhK5tYzOjhjw/K2WHriNlSY7tD/Au8ZBkM7PYSM7411GG4Ubxh60+
|
12
|
+
vamBX8rdAp5wEVKWHabdFswqtNbnmFWa00gMRA44ZMoBA5KdDCNnVA+wiA1hSLYl
|
13
|
+
SdSPrKRmYw5gRul7h8lilrvf04Df87NEtRFMjuaHcxrIklVJZMsZ1Mvw5VhlpV3f
|
14
|
+
q1kMGG3wXyeTAOlMDPEFXJ4gs8ZIEqS1T1gfCUF4w/rSDQ0u49WBu2TstQIDAQAB
|
15
|
+
MA0GCSqGSIb3DQEBCwUAA4IBAQBBsovBY2r1+PXJhGTOXvZUMqz+IN/AKi52GIwC
|
16
|
+
dPmVOhFTaztL1LbRTKgtg1cyQGmIdZ8skHGFKVAkESPa1dHu+E5uGs+rFEI1A+KA
|
17
|
+
xKIN2dNXFUEnKDEywcjZhilDHeKqthf1gkcJwkMgv5+DczOtZvtsu7tDF2kcyedw
|
18
|
+
6/A+0GJVG7S72VaL5hcfgglmGXNT5BRjLAWV6ZPViXWSJj43oXLGqqwHWhRBs+d6
|
19
|
+
0+0kNukYjyPLVSLpFpj/DvxvPjQWoDTVzMeT7iTtahd4S9FGPZuHXG2yxnTjCycH
|
20
|
+
jHNvGHXHfYJCJ10RxaeyP1Dz9qG9hH0GiiCCYAuPnf6Eu1qO
|
21
|
+
-----END CERTIFICATE-----
|
@@ -72,7 +72,7 @@ module Api
|
|
72
72
|
assert VaultConnection.exists?(@vault_connection.id)
|
73
73
|
delete :destroy, params: { id: @vault_connection.to_param }
|
74
74
|
assert_response :success
|
75
|
-
|
75
|
+
assert_not VaultConnection.exists?(@vault_connection.id)
|
76
76
|
end
|
77
77
|
end
|
78
78
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_plugin_helper'
|
4
|
+
|
5
|
+
module ForemanVault
|
6
|
+
module Orchestration
|
7
|
+
class VaultPolicyTest < ActiveSupport::TestCase
|
8
|
+
describe '#queue_vault_push' do
|
9
|
+
let(:host) { FactoryBot.create(:host, :managed) }
|
10
|
+
let(:queue) { mock('queue') }
|
11
|
+
let(:vault_policy) { mock('vault_policy') }
|
12
|
+
let(:vault_auth_method) { mock('vault_auth_method') }
|
13
|
+
let(:vault_connection) { FactoryBot.create(:vault_connection, :without_callbacks) }
|
14
|
+
|
15
|
+
setup do
|
16
|
+
host.stubs(:queue).returns(queue)
|
17
|
+
host.stubs(:vault_policy).returns(vault_policy)
|
18
|
+
host.stubs(:vault_auth_method).returns(vault_auth_method)
|
19
|
+
FactoryBot.create(:parameter, name: 'vault_connection', value: vault_connection.name)
|
20
|
+
FactoryBot.create(:setting, name: :vault_orchestration_enabled, value: true)
|
21
|
+
end
|
22
|
+
|
23
|
+
test 'should queue Vault orchestration' do
|
24
|
+
vault_policy.stubs(:valid?).returns(true)
|
25
|
+
vault_auth_method.stubs(:valid?).returns(true)
|
26
|
+
|
27
|
+
queue.expects(:create).with(
|
28
|
+
name: "Push #{host} data to Vault",
|
29
|
+
priority: 100,
|
30
|
+
action: [host, :set_vault]
|
31
|
+
).once
|
32
|
+
host.send(:queue_vault_push)
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when vault_policy is not valid' do
|
36
|
+
test 'should not queue Vault orchestration' do
|
37
|
+
vault_auth_method.stubs(:valid?).returns(true)
|
38
|
+
|
39
|
+
vault_policy.expects(:valid?).returns(false)
|
40
|
+
queue.expects(:create).never
|
41
|
+
host.send(:queue_vault_push)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when vault_auth_method is not valid' do
|
46
|
+
test 'should not queue Vault orchestration' do
|
47
|
+
vault_policy.stubs(:valid?).returns(true)
|
48
|
+
|
49
|
+
vault_auth_method.expects(:valid?).returns(false)
|
50
|
+
queue.expects(:create).never
|
51
|
+
host.send(:queue_vault_push)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe '#queue_vault_destroy' do
|
57
|
+
let(:host) { FactoryBot.create(:host, :managed) }
|
58
|
+
let(:queue) { mock('queue') }
|
59
|
+
let(:vault_policy) { mock('vault_policy') }
|
60
|
+
let(:vault_auth_method) { mock('vault_auth_method') }
|
61
|
+
let(:vault_connection) { FactoryBot.create(:vault_connection, :without_callbacks) }
|
62
|
+
|
63
|
+
setup do
|
64
|
+
host.stubs(:queue).returns(queue)
|
65
|
+
host.stubs(:vault_policy).returns(vault_policy)
|
66
|
+
host.stubs(:vault_auth_method).returns(vault_auth_method)
|
67
|
+
FactoryBot.create(:parameter, name: 'vault_connection', value: vault_connection.name)
|
68
|
+
FactoryBot.create(:setting, name: :vault_orchestration_enabled, value: true)
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'when auth_method is valid' do
|
72
|
+
test 'should queue del_vault' do
|
73
|
+
vault_auth_method.stubs(:valid?).returns(true)
|
74
|
+
|
75
|
+
queue.expects(:create).with(
|
76
|
+
name: "Clear #{host} Vault data",
|
77
|
+
priority: 60,
|
78
|
+
action: [host, :del_vault]
|
79
|
+
).once
|
80
|
+
host.send(:queue_vault_destroy)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context 'when auth_method is not valid' do
|
85
|
+
test 'should not queue del_vault' do
|
86
|
+
vault_auth_method.stubs(:valid?).returns(false)
|
87
|
+
|
88
|
+
queue.expects(:create).never
|
89
|
+
host.send(:queue_vault_destroy)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '#set_vault' do
|
95
|
+
let(:environment) { FactoryBot.create(:environment, name: 'MyEnv') }
|
96
|
+
let(:host) { FactoryBot.create(:host, :managed, environment: environment) }
|
97
|
+
let(:vault_connection) { FactoryBot.create(:vault_connection, :without_callbacks) }
|
98
|
+
let(:new_owner) { FactoryBot.create(:usergroup, name: 'MyOwner') }
|
99
|
+
|
100
|
+
let(:vault_policies) { [] }
|
101
|
+
let(:get_policies_request) do
|
102
|
+
stub_request(:get, "#{vault_connection.url}/v1/sys/policy").to_return(
|
103
|
+
status: 200, headers: { 'Content-Type': 'application/json' },
|
104
|
+
body: { policies: vault_policies }.to_json
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
let(:new_policy_name) { "#{new_owner}-#{host.environment}".parameterize }
|
109
|
+
let(:put_policy_request) do
|
110
|
+
url = "#{vault_connection.url}/v1/sys/policy/#{new_policy_name}"
|
111
|
+
# rubocop:disable Metrics/LineLength
|
112
|
+
rules = "# allow access to secrets from puppet hosts from <foreman_owner>-<puppet_environment>\npath \"secrets/data/MyOwner/MyEnv/*\" {\n capabilities = [\"create\", \"read\", \"update\"]\n}\n"
|
113
|
+
# rubocop:enable Metrics/LineLength
|
114
|
+
stub_request(:put, url).with(body: JSON.fast_generate(rules: rules)).to_return(status: 200)
|
115
|
+
end
|
116
|
+
|
117
|
+
let(:new_auth_method_name) { "#{host}-#{new_policy_name}".parameterize }
|
118
|
+
let(:post_auth_method_request) do
|
119
|
+
url = "#{vault_connection.url}/v1/auth/cert/certs/#{new_auth_method_name}"
|
120
|
+
stub_request(:post, url).with(
|
121
|
+
body: JSON.fast_generate(
|
122
|
+
certificate: host.vault_auth_method.send(:certificate),
|
123
|
+
token_policies: new_policy_name,
|
124
|
+
allowed_common_names: [host.fqdn]
|
125
|
+
)
|
126
|
+
).to_return(status: 200)
|
127
|
+
end
|
128
|
+
|
129
|
+
let(:delete_old_auth_method_request) do
|
130
|
+
url = "#{vault_connection.url}/v1/auth/cert/certs/#{host.vault_auth_method.name}"
|
131
|
+
stub_request(:delete, url).to_return(status: 200)
|
132
|
+
end
|
133
|
+
|
134
|
+
setup do
|
135
|
+
Setting.find_by(name: 'ssl_ca_file').update(value: File.join(ForemanVault::Engine.root, 'test/fixtures/ca.crt'))
|
136
|
+
FactoryBot.create(:setting, name: :vault_orchestration_enabled, value: true)
|
137
|
+
FactoryBot.create(:setting, :vault_policy)
|
138
|
+
FactoryBot.create(:provisioning_template, :vault_policy, name: Setting['vault_policy_template'])
|
139
|
+
FactoryBot.create(:parameter, name: 'vault_connection', value: vault_connection.name)
|
140
|
+
host.stubs(:skip_orchestration_for_testing?).returns(false)
|
141
|
+
|
142
|
+
get_policies_request
|
143
|
+
put_policy_request
|
144
|
+
post_auth_method_request
|
145
|
+
delete_old_auth_method_request
|
146
|
+
|
147
|
+
host.update(owner: new_owner)
|
148
|
+
end
|
149
|
+
|
150
|
+
it { assert_requested(post_auth_method_request) }
|
151
|
+
it { assert_requested(delete_old_auth_method_request) }
|
152
|
+
|
153
|
+
context 'when policy already exists on Vault' do
|
154
|
+
let(:vault_policies) { [new_policy_name] }
|
155
|
+
|
156
|
+
it { assert_not_requested(put_policy_request) }
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'when policy does not exist on Vault' do
|
160
|
+
let(:vault_policies) { [] }
|
161
|
+
|
162
|
+
it { assert_requested(put_policy_request) }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|