foreman_vault 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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'