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.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +55 -0
- data/Rakefile +47 -0
- data/app/controllers/api/v2/vault_connections_controller.rb +58 -0
- data/app/controllers/concerns/foreman_vault/controller/parameters/vault_connection.rb +23 -0
- data/app/controllers/vault_connections_controller.rb +42 -0
- data/app/jobs/refresh_vault_token.rb +28 -0
- data/app/jobs/refresh_vault_tokens.rb +25 -0
- data/app/lib/foreman_vault/macros.rb +15 -0
- data/app/models/vault_connection.rb +55 -0
- data/app/services/foreman_vault/vault_client.rb +38 -0
- data/app/views/api/v2/vault_connections/base.json.rabl +5 -0
- data/app/views/api/v2/vault_connections/create.json.rabl +5 -0
- data/app/views/api/v2/vault_connections/index.json.rabl +5 -0
- data/app/views/api/v2/vault_connections/main.json.rabl +5 -0
- data/app/views/api/v2/vault_connections/show.json.rabl +5 -0
- data/app/views/api/v2/vault_connections/update.json.rabl +5 -0
- data/app/views/vault_connections/_form.html.erb +7 -0
- data/app/views/vault_connections/edit.html.erb +3 -0
- data/app/views/vault_connections/index.html.erb +31 -0
- data/app/views/vault_connections/new.html.erb +3 -0
- data/config/foreman_vault.yaml.example +4 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20180725072913_create_vault_connection.foreman_vault.rb +15 -0
- data/db/migrate/20180809172407_rename_vault_status_to_vault_error.foreman_vault.rb +7 -0
- data/lib/foreman_vault.rb +6 -0
- data/lib/foreman_vault/engine.rb +66 -0
- data/lib/foreman_vault/version.rb +5 -0
- data/lib/tasks/foreman_vault_tasks.rake +42 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_vault.po +19 -0
- data/locale/foreman_vault.pot +19 -0
- data/locale/gemspec.rb +4 -0
- data/test/factories/foreman_vault_factories.rb +28 -0
- data/test/functional/api/v2/vault_connections_controller_test.rb +80 -0
- data/test/jobs/refresh_vault_token_test.rb +29 -0
- data/test/jobs/refresh_vault_tokens_test.rb +18 -0
- data/test/models/vault_connection_test.rb +13 -0
- data/test/test_plugin_helper.rb +9 -0
- data/test/unit/lib/foreman_vault/macros_test.rb +29 -0
- data/test/unit/services/foreman_vault/vault_client_test.rb +39 -0
- 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
|