foreman_vault 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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