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
@@ -0,0 +1,7 @@
1
+ <%= form_for @vault_connection, :url => (@vault_connection.new_record? ? vault_connections_path : vault_connection_path(:id => @vault_connection)) do |f| %>
2
+ <%= base_errors_for @vault_connection %>
3
+ <%= text_f f, :name, :help_inline => _("Vault Connection name") %>
4
+ <%= text_f f, :url, :help_inline => _("Vault Server url") %>
5
+ <%= text_f f, :token, :help_inline => _("Vault Connection token") %>
6
+ <%= submit_or_cancel f %>
7
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% title _("Edit %s") % @vault_connection.to_s %>
2
+
3
+ <%= render :partial => 'form' %>
@@ -0,0 +1,31 @@
1
+ <% title _('Vault Connections') %>
2
+ <% title_actions(new_link(_("Create Vault Connection"))) %>
3
+
4
+ <table class="<%= table_css_classes 'table-fixed' %>">
5
+ <thead>
6
+ <tr>
7
+ <th class="col-md-9"><%= sort :name, :as => s_('Vault Connections|Name') %></th>
8
+ <th class="col-md-1"><%= _('Valid') %></th>
9
+ <th class="col-md-1"><%= _('Expire time') %></th>
10
+ <th class="col-md-1"><%= _('Actions') %></th>
11
+ </tr>
12
+ </thead>
13
+ <tbody>
14
+ <% @vault_connections.each do |vault_connection| %>
15
+ <tr>
16
+ <td class="ellipsis">
17
+ <%= link_to_if_authorized vault_connection.name, hash_for_edit_vault_connection_path(:id => vault_connection) %>
18
+ </td>
19
+ <td align='center'>
20
+ <% if vault_connection.token_valid? %>
21
+ <%= ('<span class="glyphicon glyphicon-ok"/>').html_safe %>
22
+ <% else %>
23
+ <%= ('<span class="glyphicon glyphicon-remove" title="%s"/>' % vault_connection.vault_error).html_safe %>
24
+ <% end %>
25
+ </td>
26
+ <td><%= date_time_absolute(vault_connection.expire_time) %></td>
27
+ <td><%= action_buttons display_delete_if_authorized hash_for_vault_connection_path(:id => vault_connection), :data => { :confirm => _("Delete %s?") % vault_connection.name } %></td>
28
+ </tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
@@ -0,0 +1,3 @@
1
+ <% title _('Create Vault Connection') %>
2
+
3
+ <%= render :partial => 'form' %>
@@ -0,0 +1,4 @@
1
+ :foreman_vault:
2
+ :refresh_tokens_wait_time: 30
3
+ :refresh_token_retry_wait: 5
4
+ :refresh_token_retry_attempts: 3
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ resources :vault_connections, except: :show
5
+
6
+ namespace :api, defaults: { format: 'json' } do
7
+ scope '(:apiv)', module: :v2, defaults: { apiv: 'v2' }, apiv: /v1|v2/, constraints: ApiConstraints.new(version: 2, default: true) do
8
+ resources :vault_connections, only: [:index, :show, :create, :update, :destroy]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateVaultConnection < ActiveRecord::Migration[5.1]
4
+ def change
5
+ create_table :vault_connections do |t|
6
+ t.string :name
7
+ t.string :url
8
+ t.string :token
9
+ t.string :vault_status
10
+ t.datetime :expire_time
11
+
12
+ t.timestamps
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenameVaultStatusToVaultError < ActiveRecord::Migration[5.1]
4
+ def change
5
+ rename_column :vault_connections, :vault_status, :vault_error
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'foreman_vault/engine'
4
+
5
+ module ForemanVault
6
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ class Engine < ::Rails::Engine
5
+ engine_name 'foreman_vault'
6
+
7
+ config.autoload_paths += Dir["#{config.root}/app/controllers"]
8
+ config.autoload_paths += Dir["#{config.root}/app/models"]
9
+ config.autoload_paths += Dir["#{config.root}/app/services"]
10
+ config.autoload_paths += Dir["#{config.root}/app/lib"]
11
+ config.autoload_paths += Dir["#{config.root}/app/jobs"]
12
+
13
+ # Add any db migrations
14
+ initializer 'foreman_vault.load_app_instance_data' do |app|
15
+ ForemanVault::Engine.paths['db/migrate'].existent.each do |path|
16
+ app.config.paths['db/migrate'] << path
17
+ end
18
+ end
19
+
20
+ initializer 'foreman_vault.register_plugin', before: :finisher_hook do |_app|
21
+ Foreman::Plugin.register :foreman_vault do
22
+ requires_foreman '>= 1.20'
23
+
24
+ apipie_documented_controllers ["#{ForemanVault::Engine.root}/app/controllers/api/v2/*.rb"]
25
+
26
+ # Add permissions
27
+ security_block :foreman_vault do
28
+ permission :view_vault_connections, { vault_connections: [:index, :show],
29
+ 'api/v2/vault_connections': [:index, :show] }, resource_type: 'VaultConnection'
30
+ permission :create_vault_connections, { vault_connections: [:new, :create],
31
+ 'api/v2/vault_connections': [:create] }, resource_type: 'VaultConnection'
32
+ permission :edit_vault_connections, { vault_connections: [:edit, :update],
33
+ 'api/v2/vault_connections': [:update] }, resource_type: 'VaultConnection'
34
+ permission :destroy_vault_connections, { vault_connections: [:destroy],
35
+ 'api/v2/vault_connections': [:destroy] }, resource_type: 'VaultConnection'
36
+ end
37
+
38
+ # add menu entry
39
+ menu :top_menu, :vault_connections, url_hash: { controller: :vault_connections, action: :index },
40
+ caption: N_('Vault Connections'),
41
+ parent: :infrastructure_menu
42
+ end
43
+ end
44
+
45
+ config.to_prepare do
46
+ begin
47
+ Foreman::Renderer::Scope::Base.include(ForemanVault::Macros)
48
+ Foreman::Renderer.configure { |c| c.allowed_generic_helpers += [:vault_secret] }
49
+ rescue StandardError => e
50
+ Rails.logger.warn "ForemanVault: skipping engine hook (#{e})"
51
+ end
52
+ end
53
+
54
+ initializer 'foreman_vault.register_gettext', after: :load_config_initializers do |_app|
55
+ locale_dir = File.join(File.expand_path('../..', __dir__), 'locale')
56
+ locale_domain = 'foreman_vault'
57
+ Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
58
+ end
59
+
60
+ initializer 'foreman_vault.trigger_jobs', after: :load_config_initializers do |_app|
61
+ ::Foreman::Application.dynflow.config.on_init do |world|
62
+ RefreshVaultTokens.spawn_if_missing(world)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanVault
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/testtask'
4
+
5
+ # Tasks
6
+ namespace :foreman_vault do
7
+ end
8
+
9
+ # Tests
10
+ namespace :test do
11
+ desc 'Test ForemanVault'
12
+ Rake::TestTask.new(:foreman_vault) do |t|
13
+ test_dir = File.join(File.dirname(__FILE__), '../..', 'test')
14
+ t.libs << ['test', test_dir]
15
+ t.pattern = "#{test_dir}/**/*_test.rb"
16
+ t.verbose = true
17
+ t.warning = false
18
+ end
19
+ end
20
+
21
+ namespace :foreman_vault do
22
+ task :rubocop do
23
+ begin
24
+ require 'rubocop/rake_task'
25
+ RuboCop::RakeTask.new(:rubocop_foreman_vault) do |task|
26
+ task.patterns = ["#{ForemanVault::Engine.root}/app/**/*.rb",
27
+ "#{ForemanVault::Engine.root}/lib/**/*.rb",
28
+ "#{ForemanVault::Engine.root}/test/**/*.rb"]
29
+ end
30
+ rescue StandardError
31
+ puts 'Rubocop not loaded.'
32
+ end
33
+
34
+ Rake::Task['rubocop_foreman_vault'].invoke
35
+ end
36
+ end
37
+
38
+ Rake::Task[:test].enhance ['test:foreman_vault']
39
+
40
+ load 'tasks/jenkins.rake'
41
+
42
+ Rake::Task['jenkins:unit'].enhance ['test:foreman_vault', 'foreman_vault:rubocop'] if Rake::Task.task_defined?(:'jenkins:unit')
data/locale/Makefile ADDED
@@ -0,0 +1,60 @@
1
+ #
2
+ # Makefile for PO merging and MO generation. More info in the README.
3
+ #
4
+ # make all-mo (default) - generate MO files
5
+ # make check - check translations using translate-tool
6
+ # make tx-update - download and merge translations from Transifex
7
+ # make clean - clean everything
8
+ #
9
+ DOMAIN = foreman_vault
10
+ VERSION = $(shell ruby -e 'require "rubygems";spec = Gem::Specification::load(Dir.glob("../*.gemspec")[0]);puts spec.version')
11
+ POTFILE = $(DOMAIN).pot
12
+ MOFILE = $(DOMAIN).mo
13
+ POFILES = $(shell find . -name '$(DOMAIN).po')
14
+ MOFILES = $(patsubst %.po,%.mo,$(POFILES))
15
+ POXFILES = $(patsubst %.po,%.pox,$(POFILES))
16
+ EDITFILES = $(patsubst %.po,%.edit.po,$(POFILES))
17
+
18
+ %.mo: %.po
19
+ mkdir -p $(shell dirname $@)/LC_MESSAGES
20
+ msgfmt -o $(shell dirname $@)/LC_MESSAGES/$(MOFILE) $<
21
+
22
+ # Generate MO files from PO files
23
+ all-mo: $(MOFILES)
24
+
25
+ # Check for malformed strings
26
+ %.pox: %.po
27
+ msgfmt -c $<
28
+ pofilter --nofuzzy -t variables -t blank -t urls -t emails -t long -t newlines \
29
+ -t endwhitespace -t endpunc -t puncspacing -t options -t printf -t validchars --gnome $< > $@
30
+ cat $@
31
+ ! grep -q msgid $@
32
+
33
+ %.edit.po:
34
+ touch $@
35
+
36
+ check: $(POXFILES)
37
+
38
+ # Unify duplicate translations
39
+ uniq-po:
40
+ for f in $(shell find ./ -name "*.po") ; do \
41
+ msguniq $$f -o $$f ; \
42
+ done
43
+
44
+ tx-pull: $(EDITFILES)
45
+ tx pull -f
46
+ for f in $(EDITFILES) ; do \
47
+ sed -i 's/^\("Project-Id-Version: \).*$$/\1$(DOMAIN) $(VERSION)\\n"/' $$f; \
48
+ done
49
+
50
+ tx-update: tx-pull
51
+ @echo
52
+ @echo Run rake plugin:gettext[$(DOMAIN)] from the Foreman installation, then make -C locale mo-files to finish
53
+ @echo
54
+
55
+ mo-files: $(MOFILES)
56
+ git add $(POFILES) $(POTFILE) ../locale/*/LC_MESSAGES
57
+ git commit -m "i18n - pulling from tx"
58
+ @echo
59
+ @echo Changes commited!
60
+ @echo
@@ -0,0 +1,19 @@
1
+ # foreman_vault
2
+ #
3
+ # This file is distributed under the same license as foreman_vault.
4
+ #
5
+ #, fuzzy
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: version 0.0.1\n"
9
+ "Report-Msgid-Bugs-To: \n"
10
+ "POT-Creation-Date: 2014-08-20 08:46+0100\n"
11
+ "PO-Revision-Date: 2014-08-20 08:54+0100\n"
12
+ "Last-Translator: Foreman Team <foreman-dev@googlegroups.com>\n"
13
+ "Language-Team: Foreman Team <foreman-dev@googlegroups.com>\n"
14
+ "Language: \n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=UTF-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
19
+
@@ -0,0 +1,19 @@
1
+ # foreman_vault
2
+ #
3
+ # This file is distributed under the same license as foreman_vault.
4
+ #
5
+ #, fuzzy
6
+ msgid ""
7
+ msgstr ""
8
+ "Project-Id-Version: version 0.0.1\n"
9
+ "Report-Msgid-Bugs-To: \n"
10
+ "POT-Creation-Date: 2014-08-20 08:46+0100\n"
11
+ "PO-Revision-Date: 2014-08-20 08:46+0100\n"
12
+ "Last-Translator: Foreman Team <foreman-dev@googlegroups.com>\n"
13
+ "Language-Team: Foreman Team <foreman-dev@googlegroups.com>\n"
14
+ "Language: \n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=UTF-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
19
+
data/locale/gemspec.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matches foreman_vault.gemspec
4
+ _('Adds support for using credentials from Hashicorp Vault')
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ FactoryBot.define do
4
+ factory :vault_connection, class: VaultConnection do
5
+ sequence(:name) { |n| "VaultServer-#{n}" }
6
+ url 'http://localhost:8200'
7
+ token '16aa4f29-035d-b604-f3d3-8cd9a6a6921c'
8
+ expire_time { Time.zone.now + 1.year }
9
+
10
+ trait :invalid do
11
+ expire_time nil
12
+ end
13
+
14
+ trait :without_callbacks do
15
+ after(:build) do |user|
16
+ class << user
17
+ def set_expire_time
18
+ true
19
+ end
20
+
21
+ def update_expire_time
22
+ true
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ module Api
6
+ module V2
7
+ class VaultConnectionsControllerTest < ActionController::TestCase
8
+ setup do
9
+ @vault_connection = FactoryBot.create(:vault_connection, :without_callbacks)
10
+ end
11
+
12
+ describe '#index' do
13
+ test 'should get vault connections' do
14
+ get :index
15
+ response = ActiveSupport::JSON.decode(@response.body)
16
+ assert_response :success
17
+ assert response['results'].any?, 'Should respond with VaultConnections'
18
+ end
19
+ end
20
+
21
+ describe '#show' do
22
+ test 'should get vault connection detail' do
23
+ get :show, params: { id: @vault_connection.to_param }
24
+ assert_response :success
25
+ vault_connection = ActiveSupport::JSON.decode(@response.body)
26
+ assert_not vault_connection.empty?
27
+ assert_equal vault_connection['name'], @vault_connection.name
28
+ end
29
+ end
30
+
31
+ describe '#create' do
32
+ test 'should create valid' do
33
+ response = OpenStruct.new(data: { expire_time: '2018-08-01' })
34
+ auth_token = mock.tap { |object| object.expects(:lookup_self).returns(response) }
35
+ client = mock.tap { |object| object.expects(:auth_token).returns(auth_token) }
36
+ Vault::Client.expects(:new).returns(client)
37
+
38
+ params = { name: 'valid', url: 'http://localhost:8200', token: 'token' }
39
+ post :create, params: { vault_connection: params }
40
+ assert_response :success
41
+ end
42
+
43
+ test 'should not create invalid' do
44
+ post :create
45
+ assert_response :unprocessable_entity
46
+ end
47
+ end
48
+
49
+ describe '#update' do
50
+ test 'should update valid' do
51
+ response = OpenStruct.new(data: { expire_time: '2018-08-01' })
52
+ auth_token = mock.tap { |object| object.expects(:lookup_self).returns(response) }
53
+ client = mock.tap { |object| object.expects(:auth_token).returns(auth_token) }
54
+ Vault::Client.expects(:new).returns(client)
55
+
56
+ params = { name: 'New name', url: 'http://localhost:8200', token: 'token' }
57
+ put :update, params: { id: @vault_connection.to_param, vault_connection: params }
58
+ response = ActiveSupport::JSON.decode(@response.body)
59
+ assert_response :success
60
+ assert_equal params[:name], response['name']
61
+ end
62
+
63
+ test 'should not update invalid' do
64
+ params = { name: nil, url: nil, token: nil }
65
+ put :update, params: { id: @vault_connection.to_param, vault_connection: params }
66
+ assert_response :unprocessable_entity
67
+ end
68
+ end
69
+
70
+ describe '#destroy' do
71
+ test 'should destroy' do
72
+ assert VaultConnection.exists?(@vault_connection.id)
73
+ delete :destroy, params: { id: @vault_connection.to_param }
74
+ assert_response :success
75
+ refute VaultConnection.exists?(@vault_connection.id)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ class RefreshVaultTokenTest < ActiveJob::TestCase
6
+ setup do
7
+ response = OpenStruct.new(data: { expire_time: '2018-08-09' })
8
+ auth_token = mock.tap { |object| object.expects(:lookup_self).returns(response) }
9
+ client = mock.tap { |object| object.expects(:auth_token).returns(auth_token) }
10
+ Vault::Client.expects(:new).returns(client)
11
+ @vault_connection = FactoryBot.create(:vault_connection)
12
+ end
13
+
14
+ test 'should refresh vault token' do
15
+ travel_to Time.zone.parse('2018-08-08')
16
+ new_expire_time = '2018-08-10'
17
+ auth_token = mock.tap do |object|
18
+ renew_self_response = OpenStruct.new(data: nil)
19
+ object.expects(:renew_self).once.returns(renew_self_response)
20
+ lookup_self_response = OpenStruct.new(data: { expire_time: new_expire_time })
21
+ object.expects(:lookup_self).once.returns(lookup_self_response)
22
+ end
23
+ client = mock.tap { |object| object.expects(:auth_token).twice.returns(auth_token) }
24
+ Vault::Client.expects(:new).once.returns(client)
25
+
26
+ perform_enqueued_jobs { RefreshVaultToken.perform_later(@vault_connection.id) }
27
+ assert_equal Time.zone.parse(new_expire_time), @vault_connection.reload.expire_time
28
+ end
29
+ end