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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +115 -10
  3. data/app/controllers/concerns/foreman_vault/controller/parameters/vault_connection.rb +1 -1
  4. data/app/lib/foreman_vault/macros.rb +10 -1
  5. data/app/models/concerns/foreman_vault/host_extensions.rb +31 -0
  6. data/app/models/concerns/foreman_vault/orchestration/vault_policy.rb +70 -0
  7. data/app/models/concerns/foreman_vault/provisioning_template_extensions.rb +15 -0
  8. data/app/models/setting/vault.rb +85 -0
  9. data/app/models/vault_connection.rb +38 -7
  10. data/app/services/foreman_vault/vault_auth_method.rb +58 -0
  11. data/app/services/foreman_vault/vault_client.rb +24 -6
  12. data/app/services/foreman_vault/vault_policy.rb +65 -0
  13. data/app/views/unattended/provisioning_templates/VaultPolicy/default.erb +6 -0
  14. data/app/views/vault_connections/_form.html.erb +20 -4
  15. data/app/views/vault_connections/index.html.erb +21 -7
  16. data/app/views/vault_connections/welcome.html.erb +12 -0
  17. data/db/migrate/20201203220058_add_approle_to_vault_connection.rb +8 -0
  18. data/db/seeds.d/103-provisioning_templates.rb +25 -0
  19. data/lib/foreman_vault/engine.rb +15 -3
  20. data/lib/foreman_vault/version.rb +1 -1
  21. data/test/factories/{foreman_vault_factories.rb → vault_connection.rb} +3 -3
  22. data/test/factories/vault_policy_template.rb +11 -0
  23. data/test/factories/vault_setting.rb +10 -0
  24. data/test/fixtures/ca.crt +21 -0
  25. data/test/functional/api/v2/vault_connections_controller_test.rb +1 -1
  26. data/test/models/foreman_vault/orchestration/vault_policy_test.rb +167 -0
  27. data/test/models/vault_policy_template_test.rb +28 -0
  28. data/test/test_plugin_helper.rb +0 -1
  29. data/test/unit/services/foreman_vault/vault_auth_method_test.rb +130 -0
  30. data/test/unit/services/foreman_vault/vault_client_test.rb +67 -8
  31. data/test/unit/services/foreman_vault/vault_policy_test.rb +171 -0
  32. 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
- Time.zone.parse(response.data[:expire_time])
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 ||= 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
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
@@ -0,0 +1,6 @@
1
+ # NAME: <%= @host.owner %>-<%= @host.environment %>
2
+
3
+ # allow access to secrets from puppet hosts from <foreman_owner>-<puppet_environment>
4
+ path "secrets/data/<%= @host.owner %>/<%= @host.environment %>/*" {
5
+ capabilities = ["create", "read", "update"]
6
+ }
@@ -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,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,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
@@ -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
@@ -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 '>= 1.20'
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
- Foreman::Renderer::Scope::Base.include(ForemanVault::Macros)
48
- Foreman::Renderer.configure { |c| c.allowed_generic_helpers += [:vault_secret] }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanVault
4
- VERSION = '0.0.1'
4
+ VERSION = '1.0.0'
5
5
  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 nil
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.modify do
4
+ factory :setting do
5
+ trait :vault_policy do
6
+ name { 'vault_policy_template' }
7
+ value { 'Default Vault Policy' }
8
+ end
9
+ end
10
+ 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
- refute VaultConnection.exists?(@vault_connection.id)
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