foreman_webhooks 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +619 -0
  3. data/README.md +30 -0
  4. data/Rakefile +49 -0
  5. data/app/controllers/api/v2/webhook_templates_controller.rb +110 -0
  6. data/app/controllers/api/v2/webhooks_controller.rb +61 -0
  7. data/app/controllers/concerns/foreman_webhooks/controller/parameters/webhook.rb +34 -0
  8. data/app/controllers/concerns/foreman_webhooks/controller/parameters/webhook_template.rb +34 -0
  9. data/app/controllers/webhook_templates_controller.rb +5 -0
  10. data/app/controllers/webhooks_controller.rb +39 -0
  11. data/app/jobs/foreman_webhooks/deliver_webhook_job.rb +27 -0
  12. data/app/lib/foreman_webhooks/renderer/scope/webhook_template.rb +36 -0
  13. data/app/models/webhook.rb +141 -0
  14. data/app/models/webhook_template.rb +59 -0
  15. data/app/services/foreman_webhooks/webhook_service.rb +111 -0
  16. data/app/subscribers/foreman_webhooks/event_subscriber.rb +9 -0
  17. data/app/views/api/v2/webhook_templates/base.json.rabl +5 -0
  18. data/app/views/api/v2/webhook_templates/create.json.rabl +5 -0
  19. data/app/views/api/v2/webhook_templates/index.json.rabl +5 -0
  20. data/app/views/api/v2/webhook_templates/main.json.rabl +7 -0
  21. data/app/views/api/v2/webhook_templates/show.json.rabl +11 -0
  22. data/app/views/api/v2/webhook_templates/update.json.rabl +5 -0
  23. data/app/views/api/v2/webhooks/base.json.rabl +5 -0
  24. data/app/views/api/v2/webhooks/create.json.rabl +5 -0
  25. data/app/views/api/v2/webhooks/index.json.rabl +5 -0
  26. data/app/views/api/v2/webhooks/main.json.rabl +8 -0
  27. data/app/views/api/v2/webhooks/show.json.rabl +17 -0
  28. data/app/views/api/v2/webhooks/update.json.rabl +5 -0
  29. data/app/views/foreman_webhooks/webhook_templates/ansible_tower_-_host_in_inventory.erb +20 -0
  30. data/app/views/foreman_webhooks/webhook_templates/empty_payload.erb +6 -0
  31. data/app/views/foreman_webhooks/webhook_templates/webhook_template_-_payload_default.erb +11 -0
  32. data/app/views/webhook_templates/_alerts.html.erb +3 -0
  33. data/app/views/webhook_templates/_custom_tab_headers.html.erb +1 -0
  34. data/app/views/webhook_templates/_custom_tabs.html.erb +5 -0
  35. data/app/views/webhook_templates/edit.html.erb +3 -0
  36. data/app/views/webhook_templates/index.html.erb +29 -0
  37. data/app/views/webhook_templates/new.html.erb +3 -0
  38. data/app/views/webhooks/_form.html.erb +37 -0
  39. data/app/views/webhooks/_templates.html.erb +5 -0
  40. data/app/views/webhooks/edit.html.erb +3 -0
  41. data/app/views/webhooks/new.html.erb +3 -0
  42. data/config/routes.rb +45 -0
  43. data/db/migrate/20191016100128_create_webhook_targets.rb +13 -0
  44. data/db/migrate/20200831194208_rename_webhook_targets_to_webhooks.rb +7 -0
  45. data/db/migrate/20200831194514_add_template_to_webhooks.rb +7 -0
  46. data/db/migrate/20200907232758_rename_webhook_permissions.rb +22 -0
  47. data/db/migrate/20200908004234_add_columns_to_webhooks.rb +13 -0
  48. data/db/migrate/20201014115147_rename_ca_file_column.rb +7 -0
  49. data/db/migrate/20201109135301_add_http_headers.rb +8 -0
  50. data/db/seeds.d/95_webhook_templates.rb +7 -0
  51. data/lib/foreman_webhooks.rb +6 -0
  52. data/lib/foreman_webhooks/engine.rb +80 -0
  53. data/lib/foreman_webhooks/version.rb +5 -0
  54. data/lib/tasks/foreman_webhooks_tasks.rake +47 -0
  55. data/package.json +45 -0
  56. data/test/controllers/api/v2/webhook_templates_controller_test.rb +200 -0
  57. data/test/controllers/api/v2/webhooks_controller_test.rb +108 -0
  58. data/test/factories/webhook.rb +20 -0
  59. data/test/factories/webhook_target.rb +9 -0
  60. data/test/factories/webhook_template.rb +16 -0
  61. data/test/jobs/foreman_webhooks/deliver_webhook_job_test.rb +17 -0
  62. data/test/models/webhook_test.rb +38 -0
  63. data/test/test_plugin_helper.rb +8 -0
  64. data/test/unit/foreman_webhooks/webhook_service_test.rb +53 -0
  65. data/webpack/ForemanWebhooks/Routes/ForemanWebhooksRoutes.js +12 -0
  66. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/EmptyWebhooksTable/index.js +29 -0
  67. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhookDeleteModal.js +43 -0
  68. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/EnabledCell.js +16 -0
  69. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/__tests__/__snapshots__/enabledCellFormatter.test.js.snap +7 -0
  70. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/__tests__/enabledCellFormatter.test.js +7 -0
  71. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/enabledCellFormatter.js +6 -0
  72. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/Formatters/index.js +1 -0
  73. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/__tests__/EnabledCell.test.js +14 -0
  74. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/Components/__tests__/__snapshots__/EnabledCell.test.js.snap +17 -0
  75. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/WebhooksTable.js +75 -0
  76. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/WebhooksTableSchema.js +41 -0
  77. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/__tests__/WebhooksTable.test.js +57 -0
  78. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/__tests__/__snapshots__/WebhooksTable.test.js.snap +115 -0
  79. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/Components/WebhooksTable/index.js +25 -0
  80. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/WebhooksIndexPage.js +87 -0
  81. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/WebhooksIndexPage.fixtures.js +74 -0
  82. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/WebhooksIndexPage.test.js +21 -0
  83. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/__tests__/__snapshots__/WebhooksIndexPage.test.js.snap +68 -0
  84. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksIndexPage/index.js +51 -0
  85. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageActions.js +51 -0
  86. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageHelpers.js +29 -0
  87. data/webpack/ForemanWebhooks/Routes/Webhooks/WebhooksPageSelectors.js +85 -0
  88. data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/WebhooksPageHelpers.test.js +20 -0
  89. data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/WebhooksPageSelectors.test.js +45 -0
  90. data/webpack/ForemanWebhooks/Routes/Webhooks/__tests__/__snapshots__/WebhooksPageSelectors.test.js.snap +50 -0
  91. data/webpack/ForemanWebhooks/Routes/Webhooks/constants.js +13 -0
  92. data/webpack/__mocks__/foremanReact/common/HOC.js +2 -0
  93. data/webpack/__mocks__/foremanReact/common/I18n.js +7 -0
  94. data/webpack/__mocks__/foremanReact/common/helpers.js +7 -0
  95. data/webpack/__mocks__/foremanReact/common/urlHelpers.js +1 -0
  96. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalActions.js +2 -0
  97. data/webpack/__mocks__/foremanReact/components/ForemanModal/ForemanModalHooks.js +10 -0
  98. data/webpack/__mocks__/foremanReact/components/ForemanModal/index.js +18 -0
  99. data/webpack/__mocks__/foremanReact/components/Layout/LayoutActions.js +2 -0
  100. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  101. data/webpack/__mocks__/foremanReact/components/common/EmptyState.js +5 -0
  102. data/webpack/__mocks__/foremanReact/components/common/table.js +5 -0
  103. data/webpack/__mocks__/foremanReact/constants.js +24 -0
  104. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +6 -0
  105. data/webpack/__mocks__/foremanReact/redux/API/index.js +10 -0
  106. data/webpack/__mocks__/foremanReact/redux/actions/toasts.js +8 -0
  107. data/webpack/__mocks__/foremanReact/routes/common/PageLayout/PageLayout.js +10 -0
  108. data/webpack/index.js +0 -0
  109. data/webpack/routes_index.js +4 -0
  110. metadata +195 -0
@@ -0,0 +1,30 @@
1
+ # ForemanWebhooks
2
+
3
+ Don't call us, we'll call you. This plugins allows to configure Webhooks for Foreman.
4
+
5
+ ## Installation
6
+
7
+ See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
8
+ for how to install Foreman plugins
9
+
10
+ ## Contributing
11
+
12
+ Fork and send a Pull Request. Thanks!
13
+
14
+ ## Copyright
15
+
16
+ Copyright (c) 2019 Timo Goebel
17
+
18
+ This program is free software: you can redistribute it and/or modify
19
+ it under the terms of the GNU General Public License as published by
20
+ the Free Software Foundation, either version 3 of the License, or
21
+ (at your option) any later version.
22
+
23
+ This program is distributed in the hope that it will be useful,
24
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
+ GNU General Public License for more details.
27
+
28
+ You should have received a copy of the GNU General Public License
29
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
30
+
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env rake
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require 'bundler/setup'
6
+ rescue LoadError
7
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
8
+ end
9
+ begin
10
+ require 'rdoc/task'
11
+ rescue LoadError
12
+ require 'rdoc/rdoc'
13
+ require 'rake/rdoctask'
14
+ RDoc::Task = Rake::RDocTask
15
+ end
16
+
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'ForemanWebhooks'
20
+ rdoc.options << '--line-numbers'
21
+ rdoc.rdoc_files.include('README.rdoc')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
25
+ APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
26
+
27
+ Bundler::GemHelper.install_tasks
28
+
29
+ require 'rake/testtask'
30
+
31
+ Rake::TestTask.new(:test) do |t|
32
+ t.libs << 'lib'
33
+ t.libs << 'test'
34
+ t.pattern = 'test/**/*_test.rb'
35
+ t.verbose = false
36
+ end
37
+
38
+ task default: :test
39
+
40
+ begin
41
+ require 'rubocop/rake_task'
42
+ RuboCop::RakeTask.new
43
+ rescue StandardError => _e
44
+ puts 'Rubocop not loaded.'
45
+ end
46
+
47
+ task :default do
48
+ Rake::Task['rubocop'].execute
49
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V2
5
+ class WebhookTemplatesController < V2::BaseController
6
+ include Api::Version2
7
+ include ForemanWebhooks::Controller::Parameters::WebhookTemplate
8
+ include Foreman::Controller::TemplateImport
9
+
10
+ before_action :find_resource, only: %i[show update destroy clone export]
11
+
12
+ api :GET, '/webhook_templates/', N_('List webhook templates')
13
+ param_group :search_and_pagination, ::Api::V2::BaseController
14
+ add_scoped_search_description_for(WebhookTemplate)
15
+ def index
16
+ @webhook_templates = resource_scope_for_index
17
+ end
18
+
19
+ api :GET, '/webhook_templates/:id', N_('Show webhook template details')
20
+ param :id, :identifier, required: true
21
+ def show; end
22
+
23
+ def_param_group :webhook_template do
24
+ param :webhook_template, Hash, action_aware: true, required: true do
25
+ param :name, String, required: true
26
+ param :description, String
27
+ param :template, String, required: true
28
+ param :snippet, :bool, allow_nil: true
29
+ param :audit_comment, String, allow_nil: true
30
+ param :locked, :bool, desc: N_('Whether or not the template is locked for editing')
31
+ param :default, :bool, desc: N_('Whether or not the template is added automatically to new organizations and locations')
32
+ param_group :taxonomies, ::Api::V2::BaseController
33
+ end
34
+ end
35
+
36
+ def_param_group :webhook_template_clone do
37
+ param :webhook_template, Hash, required: true, action_aware: true do
38
+ param :name, String, required: true, desc: N_('Template name')
39
+ end
40
+ end
41
+
42
+ api :POST, '/webhook_templates/', N_('Create a webhook template')
43
+ param_group :webhook_template, as: :create
44
+ def create
45
+ @webhook_template = WebhookTemplate.new(webhook_template_params)
46
+ process_response @webhook_template.save
47
+ end
48
+
49
+ api :POST, '/webhook_templates/import', N_('Import a webhook template')
50
+ param :webhook_template, Hash, required: true, action_aware: true do
51
+ param :name, String, required: true, desc: N_('Template name')
52
+ param :template, String, required: true, desc: N_('Template contents including metadata')
53
+ param_group :taxonomies, ::Api::V2::BaseController
54
+ end
55
+ param_group :template_import_options, ::Api::V2::BaseController
56
+ def import
57
+ @webhook_template = WebhookTemplate.import!(*import_attrs_for(:webhook_template))
58
+ process_response @webhook_template
59
+ end
60
+
61
+ api :PUT, '/webhook_templates/:id', N_('Update a webhook template')
62
+ param :id, :identifier, required: true
63
+ param_group :webhook_template, as: :update
64
+ def update
65
+ process_response @webhook_template.update(webhook_template_params)
66
+ end
67
+
68
+ api :DELETE, '/webhook_templates/:id', N_('Delete a webhook template')
69
+ param :id, :identifier, required: true
70
+ def destroy
71
+ process_response @webhook_template.destroy
72
+ end
73
+
74
+ api :POST, '/webhook_templates/:id/clone', N_('Clone a template')
75
+ param :id, :identifier, required: true
76
+ param_group :webhook_template_clone, as: :create
77
+ def clone
78
+ @webhook_template = @webhook_template.dup
79
+ @webhook_template.name = params[:webhook_template][:name]
80
+ process_response @webhook_template.save
81
+ end
82
+
83
+ api :GET, '/webhook_templates/:id/export', N_('Export a webhook template to ERB')
84
+ param :id, :identifier, required: true
85
+ def export
86
+ send_data @webhook_template.to_erb, type: 'text/plain',
87
+ disposition: 'attachment',
88
+ filename: @webhook_template.filename
89
+ end
90
+
91
+ private
92
+
93
+ # Overload this method to avoid using search_for method
94
+ def resource_scope_for_index(options = {})
95
+ resource_scope(options).paginate(paginate_options)
96
+ end
97
+
98
+ def action_permission
99
+ case params[:action]
100
+ when 'clone', 'import'
101
+ 'create'
102
+ when 'export'
103
+ 'view'
104
+ else
105
+ super
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V2
5
+ class WebhooksController < V2::BaseController
6
+ include Api::Version2
7
+ include ForemanWebhooks::Controller::Parameters::Webhook
8
+ include Foreman::Controller::TemplateImport
9
+
10
+ before_action :find_resource, only: %i[show update destroy]
11
+
12
+ api :GET, '/webhooks/', N_('List Webhooks')
13
+ param_group :search_and_pagination, ::Api::V2::BaseController
14
+ def index
15
+ @webhooks = resource_scope_for_index
16
+ end
17
+
18
+ api :GET, '/webhooks/:id', N_('Show Webhook details')
19
+ param :id, :identifier, required: true
20
+ def show; end
21
+
22
+ def_param_group :webhook do
23
+ param :webhook, Hash, action_aware: true, required: true do
24
+ param :name, String, required: true
25
+ param :target_url, String, required: true
26
+ param :http_method, Webhook::ALLOWED_HTTP_METHODS
27
+ param :http_content_type, String
28
+ param :event, String, required: true
29
+ param :webhook_template_id, :identifier
30
+ param :enabled, :boolean
31
+ param :verify_ssl, :boolean
32
+ param :ssl_ca_certs, String, N_('X509 Certification Authorities concatenated in PEM format')
33
+ param :user, String
34
+ param :password, String
35
+ param :http_headers, String
36
+ end
37
+ end
38
+
39
+ api :POST, '/webhooks/', N_('Create a Webhook')
40
+ param_group :webhook, as: :create
41
+
42
+ def create
43
+ @webhook = Webhook.new(webhook_params)
44
+ process_response @webhook.save
45
+ end
46
+
47
+ api :PUT, '/webhooks/:id', N_('Update a Webhook')
48
+ param :id, :identifier, required: true
49
+ param_group :webhook, as: :update
50
+ def update
51
+ process_response @webhook.update(webhook_params)
52
+ end
53
+
54
+ api :DELETE, '/webhooks/:id', N_('Delete a Webhook')
55
+ param :id, :identifier, required: true
56
+ def destroy
57
+ process_response @webhook.destroy
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWebhooks
4
+ module Controller
5
+ module Parameters
6
+ module Webhook
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def webhook_params_filter
11
+ Foreman::ParameterFilter.new(::Webhook).tap do |filter|
12
+ filter.permit :name,
13
+ :target_url,
14
+ :webhook_template_id,
15
+ :event,
16
+ :http_method,
17
+ :http_content_type,
18
+ :enabled,
19
+ :verify_ssl,
20
+ :ssl_ca_certs,
21
+ :user,
22
+ :password,
23
+ :http_headers
24
+ end
25
+ end
26
+ end
27
+
28
+ def webhook_params
29
+ self.class.webhook_params_filter.filter_params(params, parameter_filter_context)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWebhooks
4
+ module Controller
5
+ module Parameters
6
+ module WebhookTemplate
7
+ extend ActiveSupport::Concern
8
+ include Foreman::Controller::Parameters::Taxonomix
9
+ include Foreman::Controller::Parameters::Template
10
+
11
+ class_methods do
12
+ def webhook_template_params_filter
13
+ Foreman::ParameterFilter.new(::WebhookTemplate).tap do |filter|
14
+ add_taxonomix_params_filter(filter)
15
+ add_template_params_filter(filter)
16
+ end
17
+ end
18
+ end
19
+
20
+ def webhook_template_params
21
+ self.class.webhook_template_params_filter.filter_params(params, parameter_filter_context)
22
+ end
23
+
24
+ def organization_params
25
+ self.class.organization_params_filter(::WebhookTemplate).filter_params(params, parameter_filter_context)
26
+ end
27
+
28
+ def location_params
29
+ self.class.location_params_filter(::WebhookTemplate).filter_params(params, parameter_filter_context)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WebhookTemplatesController < TemplatesController
4
+ include ForemanWebhooks::Controller::Parameters::WebhookTemplate
5
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WebhooksController < ::ApplicationController
4
+ include ForemanWebhooks::Controller::Parameters::Webhook
5
+ include Foreman::Controller::AutoCompleteSearch
6
+
7
+ before_action :find_resource, only: %i[edit update destroy]
8
+
9
+ def new
10
+ @webhook = Webhook.new
11
+ end
12
+
13
+ def create
14
+ @webhook = Webhook.new(webhook_params)
15
+ if @webhook.save
16
+ process_success success_redirect: '/webhooks'
17
+ else
18
+ process_error
19
+ end
20
+ end
21
+
22
+ def edit; end
23
+
24
+ def update
25
+ if @webhook.update(webhook_params)
26
+ process_success success_redirect: '/webhooks'
27
+ else
28
+ process_error
29
+ end
30
+ end
31
+
32
+ def destroy
33
+ if @webhook.destroy
34
+ process_success
35
+ else
36
+ process_error
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWebhooks
4
+ class DeliverWebhookJob < ::ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(options)
8
+ webhook = Webhook.unscoped.find_by(id: options[:webhook_id])
9
+ WebhookService.new(
10
+ webhook: webhook,
11
+ headers: options[:headers],
12
+ url: options[:url],
13
+ event_name: options[:event_name],
14
+ payload: options[:payload]
15
+ ).execute
16
+ end
17
+
18
+ def webhook_id
19
+ arguments.first['webhook_id'] unless arguments.first.nil?
20
+ end
21
+
22
+ def humanized_name
23
+ webhook = webhook_id && Webhook.unscoped.find_by(id: webhook_id)
24
+ (webhook && (_('Deliver webhook %s') % webhook.name)) || _('Deliver webhook')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWebhooks
4
+ module Renderer
5
+ module Scope
6
+ class WebhookTemplate < ::Foreman::Renderer::Scope::Template
7
+ extend ApipieDSL::Class
8
+
9
+ apipie :class, 'Macros related to Webhook payload customization' do
10
+ name 'Webhook Template'
11
+ sections only: %w[all webhooks]
12
+ end
13
+
14
+ def initialize(**args)
15
+ super
16
+ @defaults = {
17
+ event_name: @event_name,
18
+ webhook_id: @webhook_id,
19
+ context: @context
20
+ }
21
+ end
22
+
23
+ apipie :method, 'Creates final payload' do
24
+ required :hash, Hash, 'Key=value object with with data that should be present in payload'
25
+ keyword :with_defaults, [true, false], 'If set to true, adds default entries to the payload', default: true
26
+ returns String, 'JSON string with the final payload'
27
+ example 'payload({ id: @object.id, name: @object.name }) #=> "{ "id": 1, "name": "host.example.com", "context": { ... }, "event_name": "host_created.event.foreman" }"'
28
+ end
29
+ def payload(hash, with_defaults: true)
30
+ hash.merge!(@defaults) if with_defaults
31
+ hash.to_json
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Webhook < ApplicationRecord
4
+ audited
5
+ include Authorizable
6
+ include Encryptable
7
+
8
+ extend FriendlyId
9
+ friendly_id :name
10
+ include Parameterizable::ByIdName
11
+
12
+ EVENT_POSTFIX = ".#{Foreman::Observable::DEFAULT_NAMESPACE}"
13
+
14
+ EVENT_ALLOWLIST = %w[
15
+ host_created host_updated host_destroyed
16
+ hostgroup_created hostgroup_updated hostgroup_destroyed
17
+ user_created user_updated user_destroyed
18
+ domain_created domain_updated domain_destroyed
19
+ subnet_created subnet_updated subnet_destroyed
20
+ build_entered build_exited status_changed
21
+ ].map { |e| e + EVENT_POSTFIX }.freeze
22
+
23
+ DEFAULT_PAYLOAD_TEMPLATE = 'Webhook Template - Payload Default'
24
+
25
+ ALLOWED_HTTP_METHODS = %w[POST GET PUT DELETE PATCH].freeze
26
+
27
+ encrypts :password
28
+
29
+ attribute :events, :string, array: true, default: []
30
+
31
+ validates_lengths_from_database
32
+ validates :name, :target_url, :events, presence: true
33
+ validates :target_url, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]),
34
+ message: _('URL must be valid and schema must be one of: %s') % 'http, https' }
35
+ validates :http_method, inclusion: { in: ALLOWED_HTTP_METHODS }
36
+
37
+ belongs_to :webhook_template, foreign_key: :webhook_template_id
38
+
39
+ before_save :set_default_template, unless: :webhook_template
40
+
41
+ scope :for_event, ->(events) { where('events @> ARRAY[?]::varchar[]', Array(events)) }
42
+
43
+ default_scope -> { order('webhooks.name') }
44
+
45
+ scoped_search on: :name, complete_value: true, default_order: true
46
+ scoped_search on: :target_url, complete_value: :true
47
+ scoped_search on: :enabled, complete_value: { true: true, false: false }
48
+
49
+ def self.available_events
50
+ ::Foreman::EventSubscribers.all_observable_events & EVENT_ALLOWLIST
51
+ end
52
+
53
+ def self.deliver(event_name:, payload:)
54
+ for_event(event_name).includes([:webhook_template]).each do |target|
55
+ target.deliver(event_name: event_name, payload: payload) if target.enabled?
56
+ end
57
+ end
58
+
59
+ def events=(events)
60
+ events = Array(events).map do |event|
61
+ next event if event.end_with?(EVENT_POSTFIX)
62
+
63
+ event.to_s.underscore + EVENT_POSTFIX
64
+ end
65
+ super(self.class.available_events & events)
66
+ end
67
+
68
+ def event=(event)
69
+ self.events = event
70
+ end
71
+
72
+ def event
73
+ events.first
74
+ end
75
+
76
+ def deliver(event_name:, payload:)
77
+ payload_to_deliver = rendered_payload(event_name, payload)
78
+ headers_to_deliver = rendered_headers(event_name, payload)
79
+ url_to_deliver = rendered_targed_url(event_name, payload)
80
+ ::ForemanWebhooks::DeliverWebhookJob.perform_later(
81
+ event_name: event_name,
82
+ payload: payload_to_deliver,
83
+ headers: headers_to_deliver,
84
+ url: url_to_deliver,
85
+ webhook_id: id
86
+ )
87
+ end
88
+
89
+ def ca_certs_store
90
+ store = OpenSSL::X509::Store.new
91
+ return store if ssl_ca_certs.blank?
92
+
93
+ ssl_ca_certs.split(/(?=-----BEGIN)/).each do |cert|
94
+ store.add_cert(OpenSSL::X509::Certificate.new(cert))
95
+ end
96
+ store
97
+ rescue StandardError => e
98
+ raise _(format('Failed to build X509 certificate store for HTTPS client, error: %s', e.message))
99
+ end
100
+
101
+ private
102
+
103
+ def set_default_template
104
+ self.webhook_template = WebhookTemplate.find_by!(name: DEFAULT_PAYLOAD_TEMPLATE)
105
+ end
106
+
107
+ def variables(event_name, payload)
108
+ {
109
+ event_name: event_name,
110
+ object: payload[:object],
111
+ context: payload[:context],
112
+ payload: payload,
113
+ webhook_id: id
114
+ }
115
+ end
116
+
117
+ def render_source(source, event_name, payload)
118
+ scope = Foreman::Renderer.get_scope(
119
+ klass: ForemanWebhooks::Renderer::Scope::WebhookTemplate,
120
+ variables: variables(event_name, payload),
121
+ source: source
122
+ )
123
+ Foreman::Renderer.render(source, scope)
124
+ end
125
+
126
+ def rendered_payload(event_name, payload)
127
+ webhook_template.render(variables: variables(event_name, payload))
128
+ end
129
+
130
+ def rendered_headers(event_name, payload)
131
+ return nil if http_headers.empty?
132
+
133
+ source = Foreman::Renderer::Source::String.new(name: 'HTTP header template', content: http_headers)
134
+ render_source(source, event_name, payload)
135
+ end
136
+
137
+ def rendered_targed_url(event_name, payload)
138
+ source = Foreman::Renderer::Source::String.new(name: 'HTTP target URL template', content: target_url)
139
+ render_source(source, event_name, payload)
140
+ end
141
+ end