foreman_vault 0.0.1

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +55 -0
  4. data/Rakefile +47 -0
  5. data/app/controllers/api/v2/vault_connections_controller.rb +58 -0
  6. data/app/controllers/concerns/foreman_vault/controller/parameters/vault_connection.rb +23 -0
  7. data/app/controllers/vault_connections_controller.rb +42 -0
  8. data/app/jobs/refresh_vault_token.rb +28 -0
  9. data/app/jobs/refresh_vault_tokens.rb +25 -0
  10. data/app/lib/foreman_vault/macros.rb +15 -0
  11. data/app/models/vault_connection.rb +55 -0
  12. data/app/services/foreman_vault/vault_client.rb +38 -0
  13. data/app/views/api/v2/vault_connections/base.json.rabl +5 -0
  14. data/app/views/api/v2/vault_connections/create.json.rabl +5 -0
  15. data/app/views/api/v2/vault_connections/index.json.rabl +5 -0
  16. data/app/views/api/v2/vault_connections/main.json.rabl +5 -0
  17. data/app/views/api/v2/vault_connections/show.json.rabl +5 -0
  18. data/app/views/api/v2/vault_connections/update.json.rabl +5 -0
  19. data/app/views/vault_connections/_form.html.erb +7 -0
  20. data/app/views/vault_connections/edit.html.erb +3 -0
  21. data/app/views/vault_connections/index.html.erb +31 -0
  22. data/app/views/vault_connections/new.html.erb +3 -0
  23. data/config/foreman_vault.yaml.example +4 -0
  24. data/config/routes.rb +11 -0
  25. data/db/migrate/20180725072913_create_vault_connection.foreman_vault.rb +15 -0
  26. data/db/migrate/20180809172407_rename_vault_status_to_vault_error.foreman_vault.rb +7 -0
  27. data/lib/foreman_vault.rb +6 -0
  28. data/lib/foreman_vault/engine.rb +66 -0
  29. data/lib/foreman_vault/version.rb +5 -0
  30. data/lib/tasks/foreman_vault_tasks.rake +42 -0
  31. data/locale/Makefile +60 -0
  32. data/locale/en/foreman_vault.po +19 -0
  33. data/locale/foreman_vault.pot +19 -0
  34. data/locale/gemspec.rb +4 -0
  35. data/test/factories/foreman_vault_factories.rb +28 -0
  36. data/test/functional/api/v2/vault_connections_controller_test.rb +80 -0
  37. data/test/jobs/refresh_vault_token_test.rb +29 -0
  38. data/test/jobs/refresh_vault_tokens_test.rb +18 -0
  39. data/test/models/vault_connection_test.rb +13 -0
  40. data/test/test_plugin_helper.rb +9 -0
  41. data/test/unit/lib/foreman_vault/macros_test.rb +29 -0
  42. data/test/unit/services/foreman_vault/vault_client_test.rb +39 -0
  43. metadata +135 -0
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # ForemanVault
2
+
3
+ [<img src="https://opensourcelogos.aws.dmtech.cloud/dmTECH_opensource_logo.svg" height="21" width="130">](https://www.dmtech.de/)
4
+
5
+ This is a plugin for Foreman that adds support for using credentials from Hashicorp Vault.
6
+
7
+ ## Installation
8
+
9
+ See [Plugins install instructions](https://theforeman.org/plugins/) for how to install Foreman plugins.
10
+
11
+ ## Usage
12
+
13
+ Setup Vault "Dev" mode:
14
+
15
+ ```
16
+ $ brew install vault
17
+ $ vault server -dev
18
+ $ export VAULT_ADDR='http://127.0.0.1:8200'
19
+ $ vault secrets enable kv
20
+ ```
21
+
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.
23
+
24
+ You can now use `vault_secret(vault_connection_name, secret_path)` macro in your templates to fetch secrets from Vault (you can write secrets with the `$ vault write kv/my_secret foo=bar` command), e.g.
25
+
26
+ ```
27
+ <%= vault_secret('MyVault', 'kv/my_secret') %>
28
+ ```
29
+
30
+ As result you should get secret data, e.g.
31
+
32
+ ```
33
+ {:foo=>"bar"}
34
+ ```
35
+
36
+ ## Contributing
37
+
38
+ Fork and send a Pull Request. Thanks!
39
+
40
+ ## Copyright
41
+
42
+ Copyright (c) 2018 dmTECH GmbH, [dmtech.de](https://www.dmtech.de/)
43
+
44
+ This program is free software: you can redistribute it and/or modify
45
+ it under the terms of the GNU General Public License as published by
46
+ the Free Software Foundation, either version 3 of the License, or
47
+ (at your option) any later version.
48
+
49
+ This program is distributed in the hope that it will be useful,
50
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
51
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52
+ GNU General Public License for more details.
53
+
54
+ You should have received a copy of the GNU General Public License
55
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ForemanVault'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+ task default: :test
37
+
38
+ begin
39
+ require 'rubocop/rake_task'
40
+ RuboCop::RakeTask.new
41
+ rescue => _
42
+ puts 'Rubocop not loaded.'
43
+ end
44
+
45
+ task :default do
46
+ Rake::Task['rubocop'].execute
47
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V2
5
+ class VaultConnectionsController < V2::BaseController
6
+ include Api::Version2
7
+ include ForemanVault::Controller::Parameters::VaultConnection
8
+
9
+ before_action :find_resource, only: [:show, :update, :destroy]
10
+
11
+ api :GET, '/vault_connections/', N_('List VaultConnections')
12
+ param_group :search_and_pagination, ::Api::V2::BaseController
13
+ def index
14
+ @vault_connections = resource_scope_for_index
15
+ end
16
+
17
+ api :GET, '/vault_connections/:id', N_('Show VaultConnection details')
18
+ param :id, :identifier, required: true
19
+ def show; end
20
+
21
+ def_param_group :vault_connection do
22
+ param :vault_connection, Hash, action_aware: true, required: true do
23
+ param :name, String, required: true
24
+ param :url, String, required: true
25
+ param :token, String, required: true
26
+ end
27
+ end
28
+
29
+ api :POST, '/vault_connections/', N_('Create a Vault Connection')
30
+ param_group :vault_connection, as: :create
31
+
32
+ def create
33
+ @vault_connection = VaultConnection.new(vault_connection_params)
34
+ process_response @vault_connection.save
35
+ end
36
+
37
+ api :PUT, '/vault_connections/:id', N_('Update a VaultConnection')
38
+ param :id, :identifier, required: true
39
+ param_group :vault_connection
40
+ def update
41
+ process_response @vault_connection.update(vault_connection_params)
42
+ end
43
+
44
+ api :DELETE, '/vault_connections/:id', N_('Delete a VaultConnection')
45
+ param :id, :identifier, required: true
46
+ def destroy
47
+ process_response @vault_connection.destroy
48
+ end
49
+
50
+ private
51
+
52
+ # Overload this method to avoid using search_for method
53
+ def resource_scope_for_index(options = {})
54
+ resource_scope(options).paginate(paginate_options)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ module Controller
5
+ module Parameters
6
+ module VaultConnection
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def vault_connection_params_filter
11
+ Foreman::ParameterFilter.new(::VaultConnection).tap do |filter|
12
+ filter.permit :name, :url, :token
13
+ end
14
+ end
15
+ end
16
+
17
+ def vault_connection_params
18
+ self.class.vault_connection_params_filter.filter_params(params, parameter_filter_context)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VaultConnectionsController < ::ApplicationController
4
+ include ForemanVault::Controller::Parameters::VaultConnection
5
+
6
+ before_action :find_resource, only: [:edit, :update, :destroy]
7
+
8
+ def index
9
+ @vault_connections = resource_base.all
10
+ end
11
+
12
+ def new
13
+ @vault_connection = VaultConnection.new
14
+ end
15
+
16
+ def create
17
+ @vault_connection = VaultConnection.new(vault_connection_params)
18
+ if @vault_connection.save
19
+ process_success
20
+ else
21
+ process_error
22
+ end
23
+ end
24
+
25
+ def edit; end
26
+
27
+ def update
28
+ if @vault_connection.update(vault_connection_params)
29
+ process_success
30
+ else
31
+ process_error
32
+ end
33
+ end
34
+
35
+ def destroy
36
+ if @vault_connection.destroy
37
+ process_success
38
+ else
39
+ process_error
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RefreshVaultToken < ApplicationJob
4
+ def self.retry_wait
5
+ (SETTINGS&.[](:foreman_vault)&.[](:refresh_token_retry_wait) || 5).minutes
6
+ end
7
+
8
+ def self.retry_attempts
9
+ SETTINGS&.[](:foreman_vault)&.[](:refresh_token_retry_attempts) || 3
10
+ end
11
+
12
+ queue_as :vault_tokens_queue
13
+
14
+ retry_on StandardError, wait: retry_wait, attempts: retry_attempts
15
+
16
+ def perform(vault_connection_id)
17
+ vault_connection = VaultConnection.with_valid_token.find(vault_connection_id)
18
+ vault_connection.try(:renew_token!)
19
+ end
20
+
21
+ rescue_from(StandardError) do |error|
22
+ Foreman::Logging.logger('background').error("Refresh Vault token: Error #{error}: #{error.backtrace}")
23
+ end
24
+
25
+ def humanized_name
26
+ _('Refresh Vault token')
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RefreshVaultTokens < ApplicationJob
4
+ def self.wait_time
5
+ (SETTINGS&.[](:foreman_vault)&.[](:refresh_tokens_wait_time) || 30).minutes
6
+ end
7
+
8
+ queue_as :vault_tokens_queue
9
+
10
+ after_perform do
11
+ self.class.set(wait: self.class.wait_time).perform_later
12
+ end
13
+
14
+ def perform
15
+ VaultConnection.with_valid_token.each(&:perform_renew_token)
16
+ end
17
+
18
+ rescue_from(StandardError) do |error|
19
+ Foreman::Logging.logger('background').error("Refresh Vault tokens: Error #{error}: #{error.backtrace}")
20
+ end
21
+
22
+ def humanized_name
23
+ _('Refresh Vault tokens')
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ module Macros
5
+ def vault_secret(vault_connection_name, secret_path)
6
+ vault = VaultConnection.find_by!(name: vault_connection_name)
7
+ raise VaultError.new(N_('Invalid token for %s'), vault.name) unless vault.token_valid?
8
+ vault.fetch_secret(secret_path)
9
+ rescue ActiveRecord::RecordNotFound => e
10
+ raise VaultError, e.message
11
+ end
12
+
13
+ class VaultError < Foreman::Exception; end
14
+ end
15
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VaultConnection < ApplicationRecord
4
+ include Authorizable
5
+
6
+ validates_lengths_from_database
7
+ validates :name, presence: true, uniqueness: true
8
+ validates :url, :token, presence: true
9
+ validates :url, format: URI.regexp(['http', 'https'])
10
+
11
+ before_create :set_expire_time
12
+ before_update :update_expire_time
13
+
14
+ scope :with_valid_token, -> { where(vault_error: nil).where('expire_time > ?', Time.zone.now) }
15
+
16
+ delegate :fetch_expire_time, :fetch_secret, to: :client
17
+
18
+ def token_valid?
19
+ vault_error.nil? && expire_time && expire_time > Time.zone.now
20
+ end
21
+
22
+ def renew_token!
23
+ client.renew_token
24
+ save!
25
+ rescue StandardError => e
26
+ # rubocop:disable Rails/SkipsModelValidations
27
+ update_column(:vault_error, e.message)
28
+ # rubocop:enable Rails/SkipsModelValidations
29
+ end
30
+
31
+ def perform_renew_token
32
+ RefreshVaultToken.perform_later(id)
33
+ end
34
+
35
+ private
36
+
37
+ def set_expire_time
38
+ self.expire_time = fetch_expire_time
39
+ rescue StandardError => e
40
+ errors.add(:base, e.message)
41
+ throw(:abort)
42
+ end
43
+
44
+ def update_expire_time
45
+ self.expire_time = fetch_expire_time
46
+ self.vault_error = nil
47
+ rescue StandardError => e
48
+ self.expire_time = nil
49
+ self.vault_error = e.message
50
+ end
51
+
52
+ def client
53
+ @client ||= ForemanVault::VaultClient.new(url, token)
54
+ end
55
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vault'
4
+
5
+ module ForemanVault
6
+ class VaultClient
7
+ def initialize(base_url, token)
8
+ @base_url = base_url
9
+ @token = token
10
+ end
11
+
12
+ def fetch_expire_time
13
+ response = client.auth_token.lookup_self
14
+ Time.zone.parse(response.data[:expire_time])
15
+ end
16
+
17
+ def fetch_secret(secret_path)
18
+ response = client.logical.read(secret_path)
19
+ raise NoDataError.new(N_('There is no available data for path: %s'), secret_path) unless response
20
+ response.data
21
+ end
22
+
23
+ def renew_token
24
+ client.auth_token.renew_self
25
+ end
26
+
27
+ private
28
+
29
+ class VaultClientError < Foreman::Exception; end
30
+ class NoDataError < VaultClientError; end
31
+
32
+ attr_reader :base_url, :token
33
+
34
+ def client
35
+ @client ||= Vault::Client.new(address: base_url, token: token)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @vault_connection
4
+
5
+ attributes :id, :name, :url, :token, :expire_time, :vault_status
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @vault_connection
4
+
5
+ extends 'api/v2/vault_connections/show'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ collection @vault_connections
4
+
5
+ extends 'api/v2/vault_connections/main'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @vault_connection
4
+
5
+ extends 'api/v2/vault_connections/base'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @vault_connection
4
+
5
+ extends 'api/v2/vault_connections/main'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @vault_connection
4
+
5
+ extends 'api/v2/vault_connections/main'