foreman_vault 0.1.0 → 0.2.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: 340e62f20ed55a76d6d22ef7e5bf7b3b58a8197a42224019e48384fd10e8b090
4
- data.tar.gz: 5f5d0490d9d4ea43d46653abba3120b8a4e1eb08da85ff1927d1364fe7ab2de3
3
+ metadata.gz: a11fc63d2584b7fd9f4ad6b561cdbdd471503d3de307d4a77610094d67d26b62
4
+ data.tar.gz: 215b5f6e87ed318a16b7623bedccb25d17dd001ee73281f144b6726a44aee28b
5
5
  SHA512:
6
- metadata.gz: 142d5f6e82b6c30674c03653ebde20cdecece4f093f2a7b3fd0e3e621aaff4a7fa892683d69f14497d579b8b84167bbe1cc1e908eefef527e1cd26c3f49ffe15
7
- data.tar.gz: 134ba4aee2e7db7ebe52174cc936a079978daf0c2e73edab230bdeada75e6bec317589ac789e0dc5003b6d092e36f86a4e40298e316b43469030537a495a4e53
6
+ metadata.gz: b73b1a230f9982752e8ede2a38b2afd1a071a7ca397fe5f518d6447dbd30ca4f94efcbf29d4c39430cb8f8bf5b9508d9c4edd542aead2ebde60d00e82f889833
7
+ data.tar.gz: 3b85d123010e4ff958ed12885792b759cddd81b550c4f3cc287cce45b38881ea6589e700caa99751f0f0baf79002a89c2089f5a5784b23d6dafbf3509dc938cd
data/README.md CHANGED
@@ -2,31 +2,63 @@
2
2
 
3
3
  [<img src="https://opensourcelogos.aws.dmtech.cloud/dmTECH_opensource_logo.svg" height="21" width="130">](https://www.dmtech.de/)
4
4
 
5
- This is a plugin for Foreman that adds support for using credentials from Hashicorp Vault.
5
+ **Foreman Vault** is a plugin for Foreman that integrates with Hashicorp Vault for different things. Currently, it offers two distinct features.
6
6
 
7
- ## Installation
7
+ ## 1. Vault secrets in Foreman templates
8
8
 
9
- See [Plugins install instructions](https://theforeman.org/plugins/) for how to install Foreman plugins.
9
+ This adds two new macros which can be used in Foreman templates:
10
+
11
+ - `vault_secret` - Retreive secrets at a given Vault path
12
+ - `vault_issue_certificate` - Issues new certificates
13
+
14
+ ## 2. Management of Vault Policies and AuthMethods
15
+
16
+ Vault [policies](https://www.vaultproject.io/docs/concepts/policies) and [auth methods](https://www.vaultproject.io/docs/concepts/auth) (of type _cert_) can be created automatically as part of the **host orchestration**.
17
+ Auth methods also get deleted after the host is removed from Foreman.
18
+
19
+ This allows Foreman to create everything needed to access Hashicorp Vault directly from a VM using it's Puppet certificate (e.g. for _Deferred functions_ in Puppet or other CLI tools).
20
+
21
+ ## Requirements
22
+
23
+ - Foreman >= 1.20
24
+ - Working Vault instance
25
+ - with _cert_ auth enabled
26
+ - with _kv_ secret store enabled
27
+ - valid Vault Token
10
28
 
11
- ## Usage
29
+ **Dev Vault Instance**
12
30
 
13
- Setup Vault "Dev" mode:
31
+ To run a local Vault dev environment on MacOS use:
14
32
 
15
33
  ```
16
34
  $ brew install vault
17
35
  $ vault server -dev
18
36
  $ export VAULT_ADDR='http://127.0.0.1:8200'
19
37
  $ vault secrets enable kv
38
+ $ vault auth enable cert
39
+
40
+ $ vault token create -period=60m
41
+ [...]
20
42
  ```
21
43
 
22
- To set up a connection between Foreman and Vault first navigate to the "Infrastructure" > "Vault Connections" menu and then hit the button labeled "Create Vault Connection". Now you should see a form. You have to fill in name, url and token (you can receive a token with the `$ vault token create -period=60m` command) and hit the "Submit" button.
44
+ ## Installation
45
+
46
+ See [Plugins install instructions](https://theforeman.org/plugins/) for how to install Foreman plugins.
47
+
48
+ ## Basic configuration
49
+
50
+ To create a new Vault connection navigate to `Infrastructure -> Vault Connections` and hit the `Create Vault Connection` button. There you can enter a name, the Vault URL and a secret token.
51
+
52
+ ## Vault secrets in templates
23
53
 
24
- You can now utilize two new macros in your templates:
25
- - vault_secret(vault_connection_name, secret_path)
26
- - vault_issue_certificate(vault_connection_name, pki_role_path, options...)
54
+ At this point you can utilize two new macros in your templates:
55
+
56
+ - vault_secret(vault_connection_name, secret_path)
57
+ - vault_issue_certificate(vault_connection_name, pki_role_path, options...)
27
58
 
28
59
  ### vault_secret(vault_connection_name, secret_path)
29
- To fetch secrets from Vault (you can write secrets with the `$ vault write kv/my_secret foo=bar` command), e.g.
60
+
61
+ To fetch secrets from Vault (you can write secrets with the `vault write kv/my_secret foo=bar` command), e.g.
30
62
 
31
63
  ```
32
64
  <%= vault_secret('MyVault', 'kv/my_secret') %>
@@ -39,14 +71,42 @@ As result you should get secret data, e.g.
39
71
  ```
40
72
 
41
73
  ### vault_issue_certificate(vault_connection_name, pki_role_path, options...)
42
- Issueing certificates is just as easy. Be sure to have a correctly set-up PKI, meaning, configure it so you can generate certificates from within the Vault UI. This means that you'll have had to set-up a CA or Intermediate CA. Once done, you can generate a certificate like this:
43
74
 
75
+ Issueing certificates is just as easy. Be sure to have a correctly set-up PKI, meaning, configure it so you can generate certificates from within the Vault UI. This means that you'll have to set-up a CA or Intermediate CA. Once done, you can generate a certificate like this:
44
76
 
45
77
  ```
46
78
  <%= vault_issue_certificate('MyVault', 'pkiEngine/issue/testRole', common_name: 'test.mydomain.com', ttl: '10s') %>
47
79
  ```
48
80
 
49
- The common_name and ttl are optional, but there are [more options to configure](https://www.vaultproject.io/api/secret/pki/index.html#generate-certificate)
81
+ The _common_name_ and _ttl_ are optional, but there are [more options to configure](https://www.vaultproject.io/api/secret/pki/index.html#generate-certificate)
82
+
83
+ ## Vault policies and auth methods
84
+
85
+ ### Policies
86
+
87
+ The policy is based on a new template kind `VaultPolicy` which is basically a [Vault Policy](https://www.vaultproject.io/docs/concepts/policies#policy-syntax) extended with ERB.
88
+
89
+ The name of the policy is extracted from a _Magic comment_ within the template. Therefore you can use ERB to influence the name:
90
+
91
+ ```
92
+ # NAME: <%= @host.owner %>-<%= @host.environment %>
93
+
94
+ path "secret/foo" {
95
+ capabilities = ["read"]
96
+ }
97
+ ```
98
+
99
+ You can create multiple `VaultPolicy` templates and configure the default policy used in host orchestration by setting the Foreman Setting `vault_policy_template` to the desired one.
100
+
101
+ **Note: If the policy renders empty (yes, you can use conditions within ERB), the orchestration is skipped!**
102
+
103
+ ### Auth methods
104
+
105
+ [Auth methods of type `cert`](https://www.vaultproject.io/docs/auth/cert) are created with three attributes:
106
+
107
+ - **certificate**: content of the Foreman setting `ssl_ca_file`
108
+ - **allowed_common_names**: FQDN of the host which triggered the orchestration
109
+ - **token_policies**: This is automatically linked to the policy from above
50
110
 
51
111
  ## Contributing
52
112
 
@@ -63,8 +123,8 @@ the Free Software Foundation, either version 3 of the License, or
63
123
 
64
124
  This program is distributed in the hope that it will be useful,
65
125
  but WITHOUT ANY WARRANTY; without even the implied warranty of
66
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
126
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
67
127
  GNU General Public License for more details.
68
128
 
69
129
  You should have received a copy of the GNU General Public License
70
- along with this program. If not, see <http://www.gnu.org/licenses/>.
130
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ module HostExtensions
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ForemanVault::Orchestration::VaultPolicy
9
+ end
10
+
11
+ def vault_policy
12
+ VaultPolicy.new(self)
13
+ end
14
+
15
+ def vault_auth_method
16
+ VaultAuthMethod.new(self)
17
+ end
18
+
19
+ def vault_connection
20
+ return unless vault_connection_name
21
+
22
+ ::VaultConnection.find_by(name: vault_connection_name)
23
+ end
24
+
25
+ private
26
+
27
+ def vault_connection_name
28
+ params['vault_connection'] || Setting['vault_connection']
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ module Orchestration
5
+ module VaultPolicy
6
+ extend ActiveSupport::Concern
7
+
8
+ MAGIC_COMMENT_PREFIX = '# NAME: '
9
+
10
+ included do
11
+ after_validation :queue_vault_push
12
+ before_destroy :queue_vault_destroy
13
+ end
14
+
15
+ protected
16
+
17
+ def queue_vault_push
18
+ return if !managed? || errors.any?
19
+ return unless vault_policy.valid?
20
+ return unless vault_auth_method.valid?
21
+
22
+ queue.create(name: _('Push %s data to Vault') % self, priority: 100,
23
+ action: [self, :set_vault])
24
+ end
25
+
26
+ def queue_vault_destroy
27
+ return if !managed? || errors.any?
28
+
29
+ queue.create(name: _('Clear %s Vault data') % self, priority: 60,
30
+ action: [self, :del_vault])
31
+ end
32
+
33
+ # rubocop:disable Metrics/AbcSize
34
+ def set_vault
35
+ logger.info "Pushing #{name} data to Vault"
36
+
37
+ vault_policy.save if vault_policy.new?
38
+
39
+ if vault_auth_method.name != old&.vault_auth_method&.name
40
+ old&.vault_auth_method&.delete
41
+ vault_auth_method.save
42
+ end
43
+ rescue StandardError => e
44
+ Foreman::Logging.exception("Failed to push #{name} data to Vault.", e)
45
+ failure format(_('Failed to push %{name} data to Vault: %{message}\n '), name: name, message: e.message), e
46
+ end
47
+ # rubocop:enable Metrics/AbcSize
48
+
49
+ def del_vault
50
+ logger.info "Clearing #{name} Vault data"
51
+
52
+ vault_auth_method&.delete
53
+ rescue StandardError => e
54
+ Foreman::Logging.exception("Failed to clear #{name} Vault data", e)
55
+ failure format(_("Failed to clear %{name} Vault data: %{message}\n "), name: name, message: e.message), e
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Setting
4
+ class Vault < ::Setting
5
+ BLANK_ATTRS << 'vault_connection'
6
+ BLANK_ATTRS << 'vault_policy_template'
7
+
8
+ def self.default_settings
9
+ [set_vault_connection, set_vault_policy_template]
10
+ end
11
+
12
+ def self.load_defaults
13
+ # Check the table exists
14
+ return unless super
15
+
16
+ transaction do
17
+ default_settings.each { |s| create! s.update(category: 'Setting::Vault') }
18
+ end
19
+
20
+ true
21
+ end
22
+
23
+ def self.humanized_category
24
+ N_('Vault')
25
+ end
26
+
27
+ class << self
28
+ private
29
+
30
+ def set_vault_connection
31
+ set(
32
+ 'vault_connection',
33
+ N_('Default Vault Connection that can be override using parameters'),
34
+ default_vault_connection,
35
+ N_('Default Vault Connection'),
36
+ nil,
37
+ collection: vault_connections_collection,
38
+ include_blank: _('Select Vault Connection')
39
+ )
40
+ end
41
+
42
+ def default_vault_connection
43
+ return unless VaultConnection.unscoped.count == 1
44
+
45
+ VaultConnection.unscoped.first.name
46
+ end
47
+
48
+ def vault_connections_collection
49
+ proc { Hash[VaultConnection.unscoped.all.map { |vc| [vc.name, vc.name] }] }
50
+ end
51
+
52
+ def set_vault_policy_template
53
+ set(
54
+ 'vault_policy_template',
55
+ N_('The name of the ProvisioningTemplate that will be used for Vault Policy'),
56
+ default_vault_policy_template,
57
+ N_('Vault Policy template name'),
58
+ nil,
59
+ collection: vault_policy_templates_collection,
60
+ include_blank: _('Select Template')
61
+ )
62
+ end
63
+
64
+ def default_vault_policy_template
65
+ ProvisioningTemplate.unscoped.of_kind(:VaultPolicy).find_by(name: 'Default Vault Policy')&.name
66
+ end
67
+
68
+ def vault_policy_templates_collection
69
+ proc { Hash[ProvisioningTemplate.unscoped.of_kind(:VaultPolicy).map { |tmpl| [tmpl.name, tmpl.name] }] }
70
+ end
71
+ end
72
+ end
73
+ end
@@ -13,7 +13,9 @@ class VaultConnection < ApplicationRecord
13
13
 
14
14
  scope :with_valid_token, -> { where(vault_error: nil).where('expire_time > ?', Time.zone.now) }
15
15
 
16
- delegate :fetch_expire_time, :fetch_secret, :issue_certificate, to: :client
16
+ delegate :fetch_expire_time, :fetch_secret, :issue_certificate,
17
+ :policy, :policies, :put_policy, :delete_policy,
18
+ :set_certificate, :certificates, :delete_certificate, to: :client
17
19
 
18
20
  def token_valid?
19
21
  vault_error.nil? && expire_time && expire_time > Time.zone.now
@@ -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 name
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,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'vault'
4
-
5
3
  module ForemanVault
6
4
  class VaultClient
7
5
  def initialize(base_url, token)
@@ -9,6 +7,10 @@ module ForemanVault
9
7
  @token = token
10
8
  end
11
9
 
10
+ delegate :sys, :auth_tls, to: :client
11
+ delegate :policy, :policies, :put_policy, :delete_policy, to: :sys
12
+ delegate :certificate, :certificates, :set_certificate, :delete_certificate, to: :auth_tls
13
+
12
14
  def fetch_expire_time
13
15
  response = client.auth_token.lookup_self
14
16
  Time.zone.parse(response.data[:expire_time])
@@ -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 name
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
+ }
@@ -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|
@@ -44,8 +54,9 @@ 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, :vault_issue_certificate] }
57
+ ::Host::Managed.include(ForemanVault::HostExtensions)
58
+ ::Foreman::Renderer::Scope::Base.include(ForemanVault::Macros)
59
+ ::Foreman::Renderer.configure { |c| c.allowed_generic_helpers += [:vault_secret, :vault_issue_certificate] }
49
60
  rescue StandardError => e
50
61
  Rails.logger.warn "ForemanVault: skipping engine hook (#{e})"
51
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanVault
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -0,0 +1,10 @@
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 { File.read(File.join(ForemanVault::Engine.root, 'app/views/unattended/provisioning_templates/VaultPolicy/default.erb')) }
8
+ end
9
+ end
10
+ 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-----
@@ -0,0 +1,125 @@
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
+
14
+ setup do
15
+ host.stubs(:queue).returns(queue)
16
+ host.stubs(:vault_policy).returns(vault_policy)
17
+ host.stubs(:vault_auth_method).returns(vault_auth_method)
18
+ end
19
+
20
+ test 'should queue Vault orchestration' do
21
+ vault_policy.stubs(:valid?).returns(true)
22
+ vault_auth_method.stubs(:valid?).returns(true)
23
+
24
+ queue.expects(:create).with(
25
+ name: "Push #{host} data to Vault",
26
+ priority: 100,
27
+ action: [host, :set_vault]
28
+ ).once
29
+ host.send(:queue_vault_push)
30
+ end
31
+
32
+ context 'when vault_policy is not valid' do
33
+ test 'should not queue Vault orchestration' do
34
+ vault_auth_method.stubs(:valid?).returns(true)
35
+
36
+ vault_policy.expects(:valid?).returns(false)
37
+ queue.expects(:create).never
38
+ host.send(:queue_vault_push)
39
+ end
40
+ end
41
+
42
+ context 'when vault_auth_method is not valid' do
43
+ test 'should not queue Vault orchestration' do
44
+ vault_policy.stubs(:valid?).returns(true)
45
+
46
+ vault_auth_method.expects(:valid?).returns(false)
47
+ queue.expects(:create).never
48
+ host.send(:queue_vault_push)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '#set_vault' do
54
+ let(:environment) { FactoryBot.create(:environment, name: 'MyEnv') }
55
+ let(:host) { FactoryBot.create(:host, :managed, environment: environment) }
56
+ let(:vault_connection) { FactoryBot.create(:vault_connection, :without_callbacks) }
57
+ let(:new_owner) { FactoryBot.create(:usergroup, name: 'MyOwner') }
58
+
59
+ let(:vault_policies) { [] }
60
+ let(:get_policies_request) do
61
+ stub_request(:get, "#{vault_connection.url}/v1/sys/policy").to_return(
62
+ status: 200, headers: { 'Content-Type': 'application/json' },
63
+ body: { policies: vault_policies }.to_json
64
+ )
65
+ end
66
+
67
+ let(:new_policy_name) { "#{new_owner}-#{host.environment}".parameterize }
68
+ let(:put_policy_request) do
69
+ url = "#{vault_connection.url}/v1/sys/policy/#{new_policy_name}"
70
+ # rubocop:disable Metrics/LineLength
71
+ 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"
72
+ # rubocop:enable Metrics/LineLength
73
+ stub_request(:put, url).with(body: JSON.fast_generate(rules: rules)).to_return(status: 200)
74
+ end
75
+
76
+ let(:new_auth_method_name) { "#{host}-#{new_policy_name}".parameterize }
77
+ let(:post_auth_method_request) do
78
+ url = "#{vault_connection.url}/v1/auth/cert/certs/#{new_auth_method_name}"
79
+ stub_request(:post, url).with(
80
+ body: JSON.fast_generate(
81
+ certificate: host.vault_auth_method.send(:certificate),
82
+ token_policies: new_policy_name,
83
+ allowed_common_names: [host.fqdn]
84
+ )
85
+ ).to_return(status: 200)
86
+ end
87
+
88
+ let(:delete_old_auth_method_request) do
89
+ url = "#{vault_connection.url}/v1/auth/cert/certs/#{host.vault_auth_method.name}"
90
+ stub_request(:delete, url).to_return(status: 200)
91
+ end
92
+
93
+ setup do
94
+ Setting.find_by(name: 'ssl_ca_file').update(value: File.join(ForemanVault::Engine.root, 'test/fixtures/ca.crt'))
95
+ FactoryBot.create(:setting, :vault_policy)
96
+ FactoryBot.create(:provisioning_template, :vault_policy, name: Setting['vault_policy_template'])
97
+ FactoryBot.create(:parameter, name: 'vault_connection', value: vault_connection.name)
98
+ host.stubs(:skip_orchestration_for_testing?).returns(false)
99
+
100
+ get_policies_request
101
+ put_policy_request
102
+ post_auth_method_request
103
+ delete_old_auth_method_request
104
+
105
+ host.update(owner: new_owner)
106
+ end
107
+
108
+ it { assert_requested(post_auth_method_request) }
109
+ it { assert_requested(delete_old_auth_method_request) }
110
+
111
+ context 'when policy already exists on Vault' do
112
+ let(:vault_policies) { [new_policy_name] }
113
+
114
+ it { assert_not_requested(put_policy_request) }
115
+ end
116
+
117
+ context 'when policy does not exist on Vault' do
118
+ let(:vault_policies) { [] }
119
+
120
+ it { assert_requested(put_policy_request) }
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  # This calls the main test_helper in Foreman-core
4
4
  require 'test_helper'
5
- require 'vault'
6
5
 
7
6
  # Add plugin to FactoryBot's paths
8
7
  FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class VaultAuthMethodTest < ActiveSupport::TestCase
6
+ subject { ForemanVault::VaultAuthMethod.new(host) }
7
+
8
+ let(:host) { FactoryBot.create(:host, :managed) }
9
+
10
+ describe '#name' do
11
+ context 'with host and vault_policy_name' do
12
+ setup do
13
+ subject.stubs(:vault_policy_name).returns('vault_policy_name')
14
+ end
15
+
16
+ it { assert_equal "#{host}-vault_policy_name".parameterize, subject.name }
17
+ end
18
+
19
+ context 'without host' do
20
+ setup do
21
+ subject.stubs(:host).returns(nil)
22
+ subject.stubs(:vault_policy_name).returns('vault_policy_name')
23
+ end
24
+
25
+ it { assert_nil subject.name }
26
+ end
27
+
28
+ context 'without vault_policy_name' do
29
+ setup do
30
+ subject.stubs(:vault_policy_name).returns(nil)
31
+ end
32
+
33
+ it { assert_nil subject.name }
34
+ end
35
+ end
36
+
37
+ describe 'valid?' do
38
+ context 'with name and certificate' do
39
+ setup do
40
+ subject.stubs(:name).returns('name')
41
+ subject.stubs(:certificate).returns('cert')
42
+ end
43
+
44
+ it { assert subject.valid? }
45
+ end
46
+
47
+ context 'without name' do
48
+ setup do
49
+ subject.stubs(:name).returns(nil)
50
+ subject.stubs(:certificate).returns('cert')
51
+ end
52
+
53
+ it { assert_not subject.valid? }
54
+ end
55
+
56
+ context 'without certificate' do
57
+ setup do
58
+ subject.stubs(:name).returns('name')
59
+ subject.stubs(:certificate).returns(nil)
60
+ end
61
+
62
+ it { assert_not subject.valid? }
63
+ end
64
+ end
65
+
66
+ describe '#save' do
67
+ context 'when valid' do
68
+ it 'creates auth method in the Vault' do
69
+ subject.stubs(:name).returns('name')
70
+ subject.stubs(:vault_policy_name).returns('vault_policy_name')
71
+ subject.stubs(:certificate).returns('cert')
72
+
73
+ subject.expects(:set_certificate).once.with(
74
+ 'name',
75
+ certificate: 'cert',
76
+ token_policies: 'vault_policy_name',
77
+ allowed_common_names: [host.fqdn]
78
+ )
79
+ subject.save
80
+ end
81
+ end
82
+
83
+ context 'when not valid' do
84
+ it 'does not create auth method in the Vault' do
85
+ subject.stubs(:valid?).returns(false)
86
+
87
+ subject.expects(:set_certificate).never
88
+ subject.save
89
+ end
90
+ end
91
+ end
92
+
93
+ describe '#delete' do
94
+ context 'with name' do
95
+ it 'deletes Certificate' do
96
+ subject.stubs(:name).returns('name')
97
+
98
+ subject.expects(:delete_certificate).once.with(subject.name)
99
+ subject.delete
100
+ end
101
+ end
102
+
103
+ context 'without name' do
104
+ it 'does not delete Certificate' do
105
+ subject.stubs(:name).returns(nil)
106
+
107
+ subject.expects(:delete_certificate).never
108
+ subject.delete
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#certificate' do
114
+ setup do
115
+ Setting.find_by(name: 'ssl_ca_file').update(value: cert_path)
116
+ end
117
+
118
+ context 'when certificate file can be read' do
119
+ let(:cert_path) { File.join(ForemanVault::Engine.root, 'test/fixtures/ca.crt') }
120
+
121
+ it { assert_equal File.read(cert_path), subject.send(:certificate) }
122
+ end
123
+
124
+ context 'when certificate file cannot be read' do
125
+ let(:cert_path) { '/tmp/invalid.crt' }
126
+
127
+ it { assert_not subject.send(:certificate) }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class VaultPolicyTest < ActiveSupport::TestCase
6
+ subject { ForemanVault::VaultPolicy.new(host) }
7
+
8
+ let(:host) { FactoryBot.create(:host, :managed) }
9
+
10
+ setup do
11
+ FactoryBot.create(:setting, name: 'vault_policy_template', value: 'Default Vault Policy')
12
+ end
13
+
14
+ describe 'valid?' do
15
+ context 'with name and rules' do
16
+ setup do
17
+ subject.stubs(:name).returns('name')
18
+ subject.stubs(:rules).returns('rules')
19
+ end
20
+
21
+ it { assert subject.valid? }
22
+ end
23
+
24
+ context 'without name' do
25
+ setup do
26
+ subject.stubs(:name).returns(nil)
27
+ subject.stubs(:rules).returns('rules')
28
+ end
29
+
30
+ it { assert_not subject.valid? }
31
+ end
32
+
33
+ context 'without rules' do
34
+ setup do
35
+ subject.stubs(:name).returns('name')
36
+ subject.stubs(:rules).returns(nil)
37
+ end
38
+
39
+ it { assert_not subject.valid? }
40
+ end
41
+ end
42
+
43
+ describe '#name' do
44
+ context 'without corresponding Vault Policy template' do
45
+ it { assert_nil subject.name }
46
+ end
47
+
48
+ context 'with corresponding Vault Policy template' do
49
+ setup do
50
+ FactoryBot.create(:provisioning_template, :vault_policy, template: template)
51
+ end
52
+
53
+ let(:template) { '# NAME: <%= @host.name %>' }
54
+
55
+ it { assert_equal host.name.parameterize, subject.name }
56
+
57
+ context 'when name is empty' do
58
+ let(:template) { '# NAME:' }
59
+
60
+ it { assert_nil subject.name }
61
+ end
62
+
63
+ context 'when there is no name magic comment' do
64
+ let(:template) { '# BLAH:' }
65
+
66
+ it { assert_nil subject.name }
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#new?' do
72
+ setup do
73
+ FactoryBot.create(:provisioning_template, :vault_policy)
74
+ end
75
+
76
+ context 'policy already exists in the Vault' do
77
+ setup do
78
+ subject.stubs(:policies).returns([subject.name])
79
+ end
80
+
81
+ it { assert_not subject.new? }
82
+ end
83
+
84
+ context 'policy does not exist in the Vault' do
85
+ setup do
86
+ subject.stubs(:policies).returns([])
87
+ end
88
+
89
+ it { assert subject.new? }
90
+ end
91
+ end
92
+
93
+ describe '#save' do
94
+ context 'when valid' do
95
+ it 'creates Vault Policy' do
96
+ subject.stubs(:name).returns('name')
97
+ subject.stubs(:rules).returns('rules')
98
+
99
+ subject.expects(:put_policy).once.with(subject.name, subject.send(:rules))
100
+ subject.save
101
+ end
102
+ end
103
+
104
+ context 'when not valid' do
105
+ it 'does not create Vault Policy' do
106
+ subject.stubs(:valid?).returns(false)
107
+
108
+ subject.expects(:set_certificate).never
109
+ subject.save
110
+ end
111
+ end
112
+ end
113
+
114
+ describe '#delete' do
115
+ context 'with name' do
116
+ it 'deletes Vault Policy' do
117
+ subject.stubs(:name).returns('name')
118
+
119
+ subject.expects(:delete_policy).once.with(subject.name)
120
+ subject.delete
121
+ end
122
+ end
123
+
124
+ context 'without name' do
125
+ it 'does not delete Vault Policy' do
126
+ subject.stubs(:name).returns(nil)
127
+
128
+ subject.expects(:delete_policy).never
129
+ subject.delete
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '#rules' do
135
+ context 'without corresponding Vault Policy template' do
136
+ it { assert_nil subject.send(:rules) }
137
+ end
138
+
139
+ context 'with corresponding Vault Policy template' do
140
+ let(:rules) { 'path "secrets/data/*" { capabilities = ["create", "read", "update"] }' }
141
+
142
+ setup do
143
+ FactoryBot.create(:provisioning_template, :vault_policy, template: template)
144
+ end
145
+
146
+ let(:template) do
147
+ <<~TEMPLATE
148
+ # NAME: <%= @host.name %>
149
+
150
+ #{rules}
151
+ TEMPLATE
152
+ end
153
+
154
+ it { assert_equal "#{rules}\n", subject.send(:rules) }
155
+
156
+ context 'when Vault Policy template renders empty' do
157
+ let(:template) do
158
+ <<~TEMPLATE
159
+ # NAME: <%= @host.name %>
160
+
161
+ <% if false %>
162
+ #{rules}
163
+ <% end %>
164
+ TEMPLATE
165
+ end
166
+
167
+ it { assert_nil subject.send(:rules) }
168
+ end
169
+ end
170
+ end
171
+ 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.1.0
4
+ version: 0.2.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-03-02 00:00:00.000000000 Z
11
+ date: 2020-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: vault
@@ -68,14 +68,20 @@ files:
68
68
  - app/jobs/refresh_vault_token.rb
69
69
  - app/jobs/refresh_vault_tokens.rb
70
70
  - app/lib/foreman_vault/macros.rb
71
+ - app/models/concerns/foreman_vault/host_extensions.rb
72
+ - app/models/concerns/foreman_vault/orchestration/vault_policy.rb
73
+ - app/models/setting/vault.rb
71
74
  - app/models/vault_connection.rb
75
+ - app/services/foreman_vault/vault_auth_method.rb
72
76
  - app/services/foreman_vault/vault_client.rb
77
+ - app/services/foreman_vault/vault_policy.rb
73
78
  - app/views/api/v2/vault_connections/base.json.rabl
74
79
  - app/views/api/v2/vault_connections/create.json.rabl
75
80
  - app/views/api/v2/vault_connections/index.json.rabl
76
81
  - app/views/api/v2/vault_connections/main.json.rabl
77
82
  - app/views/api/v2/vault_connections/show.json.rabl
78
83
  - app/views/api/v2/vault_connections/update.json.rabl
84
+ - app/views/unattended/provisioning_templates/VaultPolicy/default.erb
79
85
  - app/views/vault_connections/_form.html.erb
80
86
  - app/views/vault_connections/edit.html.erb
81
87
  - app/views/vault_connections/index.html.erb
@@ -85,6 +91,7 @@ files:
85
91
  - config/routes.rb
86
92
  - db/migrate/20180725072913_create_vault_connection.foreman_vault.rb
87
93
  - db/migrate/20180809172407_rename_vault_status_to_vault_error.foreman_vault.rb
94
+ - db/seeds.d/103-provisioning_templates.rb
88
95
  - lib/foreman_vault.rb
89
96
  - lib/foreman_vault/engine.rb
90
97
  - lib/foreman_vault/version.rb
@@ -93,14 +100,20 @@ files:
93
100
  - locale/en/foreman_vault.po
94
101
  - locale/foreman_vault.pot
95
102
  - locale/gemspec.rb
96
- - test/factories/foreman_vault_factories.rb
103
+ - test/factories/vault_connection.rb
104
+ - test/factories/vault_policy_template.rb
105
+ - test/factories/vault_setting.rb
106
+ - test/fixtures/ca.crt
97
107
  - test/functional/api/v2/vault_connections_controller_test.rb
98
108
  - test/jobs/refresh_vault_token_test.rb
99
109
  - test/jobs/refresh_vault_tokens_test.rb
110
+ - test/models/foreman_vault/orchestration/vault_policy_test.rb
100
111
  - test/models/vault_connection_test.rb
101
112
  - test/test_plugin_helper.rb
102
113
  - test/unit/lib/foreman_vault/macros_test.rb
114
+ - test/unit/services/foreman_vault/vault_auth_method_test.rb
103
115
  - test/unit/services/foreman_vault/vault_client_test.rb
116
+ - test/unit/services/foreman_vault/vault_policy_test.rb
104
117
  homepage: https://github.com/dm-drogeriemarkt/foreman_vault
105
118
  licenses:
106
119
  - GPL-3.0
@@ -120,16 +133,21 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
133
  - !ruby/object:Gem::Version
121
134
  version: '0'
122
135
  requirements: []
123
- rubyforge_project:
124
- rubygems_version: 2.7.6.2
136
+ rubygems_version: 3.1.2
125
137
  signing_key:
126
138
  specification_version: 4
127
139
  summary: Adds support for using credentials from Hashicorp Vault
128
140
  test_files:
129
141
  - test/unit/lib/foreman_vault/macros_test.rb
130
142
  - test/unit/services/foreman_vault/vault_client_test.rb
143
+ - test/unit/services/foreman_vault/vault_policy_test.rb
144
+ - test/unit/services/foreman_vault/vault_auth_method_test.rb
131
145
  - test/models/vault_connection_test.rb
132
- - test/factories/foreman_vault_factories.rb
146
+ - test/models/foreman_vault/orchestration/vault_policy_test.rb
147
+ - test/factories/vault_policy_template.rb
148
+ - test/factories/vault_connection.rb
149
+ - test/factories/vault_setting.rb
150
+ - test/fixtures/ca.crt
133
151
  - test/test_plugin_helper.rb
134
152
  - test/jobs/refresh_vault_tokens_test.rb
135
153
  - test/jobs/refresh_vault_token_test.rb