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
@@ -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