Skip to content

Commit 3e1c562

Browse files
committed
feat: only log webhook response details when a webhook host whitelist has been configured
1 parent 077e37f commit 3e1c562

14 files changed

+178
-40
lines changed

lib/pact_broker/api/decorators/webhook_execution_result_decorator.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
require_relative 'base_decorator'
22
require 'json'
3+
require 'pact_broker/messages'
34

45
module PactBroker
56
module Api
67
module Decorators
7-
88
class WebhookExecutionResultDecorator < BaseDecorator
9-
109
class ErrorDecorator < BaseDecorator
11-
1210
property :message
1311
property :backtrace
14-
1512
end
1613

1714
class HTTPResponseDecorator < BaseDecorator
18-
1915
property :status, :getter => lambda { |_| code.to_i }
2016
property :headers, exec_context: :decorator
2117
property :body, exec_context: :decorator
@@ -34,11 +30,11 @@ def body
3430
represented.body
3531
end
3632
end
37-
3833
end
3934

40-
property :error, :extend => ErrorDecorator
41-
property :response, :extend => HTTPResponseDecorator
35+
property :error, :extend => ErrorDecorator, if: lambda { |context| context[:options][:user_options][:show_response] }
36+
property :response, :extend => HTTPResponseDecorator, if: lambda { |context| context[:options][:user_options][:show_response] }
37+
property :response_hidden_message, as: :message, exec_context: :decorator, if: lambda { |context| !context[:options][:user_options][:show_response] }
4238

4339
link :webhook do | options |
4440
{
@@ -52,7 +48,11 @@ def body
5248
href: webhook_execution_url(options.fetch(:webhook), options.fetch(:base_url))
5349
}
5450
end
51+
52+
def response_hidden_message
53+
PactBroker::Messages.message('messages.response_body_hidden')
54+
end
5555
end
5656
end
5757
end
58-
end
58+
end

lib/pact_broker/api/resources/webhook_execution.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def resource_exists?
3232
private
3333

3434
def post_response_body webhook_execution_result
35-
Decorators::WebhookExecutionResultDecorator.new(webhook_execution_result).to_json(user_options: { base_url: base_url, webhook: webhook })
35+
Decorators::WebhookExecutionResultDecorator.new(webhook_execution_result).to_json(user_options: user_options)
3636
end
3737

3838
def webhook
@@ -46,8 +46,11 @@ def pact
4646
def uuid
4747
identifier_from_path[:uuid]
4848
end
49+
50+
def user_options
51+
{ base_url: base_url, webhook: webhook, show_response: PactBroker.configuration.show_webhook_response? }
52+
end
4953
end
5054
end
5155
end
52-
5356
end

lib/pact_broker/configuration.rb

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ def add_api_error_reporter &block
146146
end
147147
end
148148

149+
def show_webhook_response?
150+
webhook_host_whitelist.any?
151+
end
152+
149153
def enable_badge_resources= enable_badge_resources
150154
puts "Pact Broker configuration property `enable_badge_resources` is deprecated. Please use `enable_public_badge_access`"
151155
self.enable_public_badge_access = enable_badge_resources

lib/pact_broker/doc/views/webhooks.markdown

+14-1
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,23 @@ A request body can be specified as well.
5959

6060
To ensure that webhooks cannot be used maliciously to expose either data about your contracts or your internal network, the following validation rules are applied to webhooks via the Pact Broker configuration settings.
6161

62-
* **Scheme**: Must be included in the `webhook_scheme_whitelist`, which by default only includes `https`. You can change this to include `http` if absolutely necessary, however, keep in mind that the body of any http traffic is visible to the network. You can load a self signed certificate into the Pact Broker to be used for https connections using `script/insert-self-signed-certificate-from-url.rb` in the Pact Broker repository.
62+
* **Scheme**: Must be included in the `webhook_scheme_whitelist`, which by default only includes `https`. You can change this to include `http` if absolutely necessary, however, keep in mind that the body of any http traffic is visible to the network. You can load a self signed certificate into the Pact Broker to be used for https connections using [script/insert-self-signed-certificate-from-url.rb](https://github.com/pact-foundation/pact_broker/blob/master/script/insert-self-signed-certificate-from-url.rb) in the
63+
Pact Broker Github repository.
6364

6465
* **HTTP method**: Must be included in the `webhook_http_method_whitelist`, which by default only includes `POST`. It is highly recommended that only `POST` requests are allowed to ensure that webhooks cannot be used to retrieve sensitive information from hosts within the same network.
6566

67+
* **Host**: If the `webhook_host_whitelist` contains any entries, the host must match one or more of the entries. By default, it is empty. For security purposes, if the host whitelist is empty, the response details will not be logged to the UI (though they can be seen in the application logs at debug level).
68+
69+
The host whitelist may contain hostnames (eg `"github.com"`), IPs (eg `"192.0.345.4"`), network ranges (eg `"10.0.0.0/8"`) or regular expressions (eg `/.*\.foo\.com$/`). Note that IPs are not resolved, so if you specify an IP range, you need to use the IP in the webhook URL. If you wish to allow webhooks to any host (not recommended!), you can set `webhook_host_whitelist` to `[/.*/]`. Beware of any sensitive endpoints that may be exposed within the same network.
70+
71+
The recommended set of values to start with are:
72+
73+
* your CI server's hostname (for triggering builds)
74+
* your company chat (eg. Slack, for publishing notifications)
75+
* your code repository (eg. Github, for sending commit statuses)
76+
77+
Alternatively, you could use a regular expression to limit requests to your company's domain. eg `/.*\.foo\.com$/` (don't forget the end of string anchor). You can test Ruby regular expressions at [rubular.com](http://rubular.com).
78+
6679
#### Event types
6780

6881
`contract_content_changed:` triggered when the content of the contract has changed since the previous publication. Uses plain string equality, so changes to the ordering of hash keys, or whitespace changes will trigger this webhook.

lib/pact_broker/domain/webhook_request.rb

+34-7
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,14 @@ def execute_and_build_result pact, options, logs, execution_logger
6868
uri = build_uri(pact)
6969
req = build_request(uri, pact, execution_logger)
7070
response = do_request(uri, req)
71-
log_response(response, execution_logger)
71+
log_response(response, execution_logger, options)
7272
result = WebhookExecutionResult.new(response, logs.string)
7373
log_completion_message(options, execution_logger, result.success?)
7474
result
7575
end
7676

7777
def handle_error_and_build_result e, options, logs, execution_logger
78-
logger.error "Error executing webhook #{uuid} #{e.class.name} - #{e.message} #{e.backtrace.join("\n")}"
79-
execution_logger.error "Error executing webhook #{uuid} #{e.class.name} - #{e.message}"
78+
log_error(e, execution_logger, options)
8079
log_completion_message(options, execution_logger, false)
8180
WebhookExecutionResult.new(nil, logs.string, e)
8281
end
@@ -96,7 +95,7 @@ def build_request uri, pact, execution_logger
9695
req.body = PactBroker::Webhooks::Render.call(String === body ? body : body.to_json, pact)
9796
end
9897

99-
execution_logger.info req.body
98+
execution_logger.info(req.body) if req.body
10099
req
101100
end
102101

@@ -108,15 +107,33 @@ def do_request uri, req
108107
end
109108
end
110109

111-
def log_response response, execution_logger
112-
execution_logger.info(" ")
110+
def log_response response, execution_logger, options
111+
log_response_to_application_logger(response)
112+
if options[:show_response]
113+
log_response_to_execution_logger(response, execution_logger)
114+
else
115+
execution_logger.info response_body_hidden_message
116+
end
117+
end
118+
119+
def response_body_hidden_message
120+
PactBroker::Messages.message('messages.response_body_hidden')
121+
end
122+
123+
def log_response_to_application_logger response
113124
logger.info "Received response for webhook #{uuid} status=#{response.code}"
125+
logger.debug "headers=#{response.to_hash}"
126+
logger.debug "body=#{response.body}"
127+
end
128+
129+
def log_response_to_execution_logger response, execution_logger
114130
execution_logger.info "HTTP/#{response.http_version} #{response.code} #{response.message}"
115131
response.each_header do | header |
116132
execution_logger.info "#{header.split("-").collect(&:capitalize).join('-')}: #{response[header]}"
117133
end
118-
logger.debug "body=#{response.body}"
134+
119135
safe_body = nil
136+
120137
if response.body
121138
safe_body = response.body.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
122139
if response.body != safe_body
@@ -138,6 +155,16 @@ def log_completion_message options, execution_logger, success
138155
end
139156
end
140157

158+
def log_error e, execution_logger, options
159+
logger.error "Error executing webhook #{uuid} #{e.class.name} - #{e.message} #{e.backtrace.join("\n")}"
160+
161+
if options[:show_response]
162+
execution_logger.error "Error executing webhook #{uuid} #{e.class.name} - #{e.message}"
163+
else
164+
execution_logger.error "Error executing webhook #{uuid}. #{response_body_hidden_message}"
165+
end
166+
end
167+
141168
def to_s
142169
"#{method.upcase} #{url}, username=#{username}, password=#{display_password}, headers=#{headers}, body=#{body}"
143170
end

lib/pact_broker/locale/en.yml

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ en:
99
valid_consumer_version_number?: "Consumer version number '%{value}' cannot be parsed to a version number. The expected format (unless this configuration has been overridden) is a semantic version. eg. 1.3.0 or 2.0.4.rc1"
1010

1111
pact_broker:
12+
messages:
13+
response_body_hidden: For security purposes, the response details are not logged. To enable response logging, configure the webhook_host_whitelist property. See /doc/webhooks#whitelist for more information.
1214
errors:
1315
validation:
1416
blank: "cannot be blank."

lib/pact_broker/webhooks/service.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ def self.find_all
6161

6262
def self.execute_webhook_now webhook, pact
6363
triggered_webhook = webhook_repository.create_triggered_webhook(next_uuid, webhook, pact, USER)
64-
webhook_execution_result = execute_triggered_webhook_now triggered_webhook, failure_log_message: "Webhook execution failed"
64+
options = { failure_log_message: "Webhook execution failed", show_response: PactBroker.configuration.show_webhook_response?}
65+
webhook_execution_result = execute_triggered_webhook_now triggered_webhook, options
6566
if webhook_execution_result.success?
6667
webhook_repository.update_triggered_webhook_status triggered_webhook, TriggeredWebhook::STATUS_SUCCESS
6768
else

lib/pact_broker/webhooks/webhook.rb

-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
module PactBroker
66
module Webhooks
77
class Webhook < Sequel::Model
8-
98
set_primary_key :id
109
associate(:many_to_one, :provider, :class => "PactBroker::Domain::Pacticipant", :key => :provider_id, :primary_key => :id)
1110
associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id)
@@ -84,13 +83,11 @@ def self.properties_hash_from_domain webhook
8483
is_json_request_body: is_json_request_body
8584
}
8685
end
87-
8886
end
8987

9088
Webhook.plugin :timestamps, :update_on_create=>true
9189

9290
class WebhookHeader < Sequel::Model
93-
9491
associate(:many_to_one, :webhook, :class => "PactBroker::Repositories::Webhook", :key => :webhook_id, :primary_key => :id)
9592

9693
def self.from_domain name, value, webhook_id
@@ -100,8 +97,6 @@ def self.from_domain name, value, webhook_id
10097
db_header.webhook_id = webhook_id
10198
db_header
10299
end
103-
104100
end
105101
end
106-
107102
end

spec/features/create_webhook_spec.rb

-1
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,4 @@
7676
expect(response_body).to include webhook_hash
7777
end
7878
end
79-
8079
end

spec/features/execute_webhook_spec.rb

+32-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323

2424
context "when the execution is successful" do
2525
let!(:request) do
26-
stub_request(:post, /http/).with(body: 'http://example.org/pacts/provider/Bar/consumer/Foo/version/1').to_return(:status => 200)
26+
stub_request(:post, /http/).with(body: 'http://example.org/pacts/provider/Bar/consumer/Foo/version/1').to_return(:status => 200, body: response_body)
2727
end
28+
let(:response_body) { "webhook-response-body" }
2829

2930
it "performs the HTTP request" do
3031
subject
@@ -42,6 +43,36 @@
4243
it "saves a WebhookExecution" do
4344
expect { subject }.to change { PactBroker::Webhooks::Execution.count }.by(1)
4445
end
46+
47+
context "when a webhook host whitelist is not configured" do
48+
before do
49+
allow(PactBroker.configuration).to receive(:show_webhook_response?).and_return(false)
50+
end
51+
52+
it "does not show any response details" do
53+
expect(subject.body).to_not include response_body
54+
end
55+
56+
it "does not log any response details" do
57+
subject
58+
expect(PactBroker::Webhooks::Execution.order(:id).last.logs).to_not include response_body
59+
end
60+
end
61+
62+
context "when a webhook host whitelist is configured" do
63+
before do
64+
allow(PactBroker.configuration).to receive(:show_webhook_response?).and_return(true)
65+
end
66+
67+
it "includes response details" do
68+
expect(subject.body).to include response_body
69+
end
70+
71+
it "logs the response details" do
72+
subject
73+
expect(PactBroker::Webhooks::Execution.order(:id).last.logs).to include response_body
74+
end
75+
end
4576
end
4677

4778
context "when an error occurs", no_db_clean: true do

spec/lib/pact_broker/api/decorators/webhook_execution_result_decorator_spec.rb

+15-3
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ module Decorators
1414
let(:response) { double('http_response', code: '200', body: response_body, to_hash: headers) }
1515
let(:response_body) { 'body' }
1616
let(:error) { nil }
17-
let(:webhook) { instance_double(PactBroker::Domain::Webhook, uuid: 'some-uuid')}
17+
let(:webhook) { instance_double(PactBroker::Domain::Webhook, uuid: 'some-uuid') }
18+
let(:show_response) { true }
1819
let(:json) {
1920
WebhookExecutionResultDecorator.new(webhook_execution_result)
20-
.to_json(user_options: { base_url: 'http://example.org', webhook: webhook })
21+
.to_json(user_options: { base_url: 'http://example.org', webhook: webhook, show_response: show_response })
2122
}
2223

2324
let(:subject) { JSON.parse(json, symbolize_names: true)}
@@ -62,8 +63,19 @@ module Decorators
6263
end
6364
end
6465
end
65-
end
6666

67+
context "when show_response is false" do
68+
let(:show_response) { false }
69+
70+
it "does not include the response" do
71+
expect(subject).to_not have_key(:response)
72+
end
73+
74+
it "includes a message about why the response is hidden" do
75+
expect(subject[:message]).to match /security purposes/
76+
end
77+
end
78+
end
6779
end
6880
end
6981
end

spec/lib/pact_broker/api/resources/webhook_execution_spec.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ module Resources
5757
end
5858

5959
it "generates a JSON response body for the execution result" do
60-
expect(decorator).to receive(:to_json).with(user_options: { base_url: 'http://example.org', webhook: webhook })
60+
allow(PactBroker.configuration).to receive(:show_webhook_response?).and_return('foo')
61+
expect(decorator).to receive(:to_json).with(user_options: { base_url: 'http://example.org', webhook: webhook, show_response: 'foo' })
6162
subject
6263
end
6364

@@ -69,6 +70,7 @@ module Resources
6970

7071
context "when execution is not successful" do
7172
let(:success) { false }
73+
7274
it "returns a 500 JSON response" do
7375
subject
7476
expect(last_response.status).to eq 500

0 commit comments

Comments
 (0)