diff --git a/.gitignore b/.gitignore index 56beec3..469dbb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ pact_broker/pact_broker.sqlite pact_broker.sqlite +pact_broker/log diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..49d5710 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--format documentation diff --git a/Gemfile b/Gemfile index c681c3c..2025a0f 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source 'https://rubygems.org' gem 'rake', '~> 12.0' gem 'conventional-changelog', '~>1.3' +gem 'rspec', '~> 3.7' +gem 'rspec-its', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 6bf0c08..9474823 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,24 @@ GEM remote: https://rubygems.org/ specs: conventional-changelog (1.3.0) + diff-lcs (1.3) rake (12.3.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-its (1.2.0) + rspec-core (>= 3.0.0) + rspec-expectations (>= 3.0.0) + rspec-mocks (3.7.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) PLATFORMS ruby @@ -10,6 +27,8 @@ PLATFORMS DEPENDENCIES conventional-changelog (~> 1.3) rake (~> 12.0) + rspec (~> 3.7) + rspec-its (~> 1.2) BUNDLED WITH - 1.16.0 + 1.16.2 diff --git a/README.md b/README.md index 02fb224..8f341c0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,15 @@ See the [Pact Broker configuration documentation][reverse-proxy]. Set the environment variable `PACT_BROKER_LOG_LEVEL` to one of `DEBUG`, `INFO`, `WARN`, `ERROR`, or `FATAL`. +## Webhook whitelists + +* PACT_BROKER_WEBHOOK_HOST_WHITELIST - a space delimited list of hosts (eg. `github.com`), network ranges (eg. `10.2.3.41/24`, or regular expressions (eg. `/.*\\.foo\\.com$/`). Regular expressions should start and end with a `/` to differentiate them from Strings. Note that backslashes need to be escaped with a second backslash. Please read the [Webhook whitelists](https://github.com/pact-foundation/pact_broker/wiki/Configuration#webhook-whitelists) section of the Pact Broker configuration documentation to understand how the whitelist is used. Remember to use quotes around this value as it may have spaces in it. +* PACT_BROKER_WEBHOOK_SCHEME_WHITELIST - a space delimited list (eg. `http https`). Defaults to `https`. + +## Other environment variables + +* PACT_BROKER_BASE_EQUALITY_ONLY_ON_CONTENT_THAT_AFFECTS_VERIFICATION_RESULTS - `true` by default, may be set to `false`. + ## General Pact Broker configuration and usage Documentation for the Pact Broker application itself can be found in the Pact Broker [wiki][pact-broker-wiki]. diff --git a/Rakefile b/Rakefile index ed5daae..25921b4 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,11 @@ require 'conventional_changelog' +require 'rspec/core' +require 'rspec/core/rake_task' task :generate_changelog do ConventionalChangelog::Generator.new.generate! version: ENV.fetch('TAG') end + +RSpec::Core::RakeTask.new(:spec) + +task :default => :spec diff --git a/container/etc/nginx/main.d/pactbroker-env.conf b/container/etc/nginx/main.d/pactbroker-env.conf index 4cc4a27..eed9a0c 100644 --- a/container/etc/nginx/main.d/pactbroker-env.conf +++ b/container/etc/nginx/main.d/pactbroker-env.conf @@ -8,6 +8,10 @@ env PACT_BROKER_BASIC_AUTH_USERNAME; env PACT_BROKER_BASIC_AUTH_PASSWORD; env PACT_BROKER_PUBLIC_HEARTBEAT; env PACT_BROKER_LOG_LEVEL; +env PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST; +env PACT_BROKER_WEBHOOK_SCHEME_WHITELIST; +env PACT_BROKER_WEBHOOK_HOST_WHITELIST; +env PACT_BROKER_BASE_EQUALITY_ONLY_ON_CONTENT_THAT_AFFECTS_VERIFICATION_RESULTS; env http_proxy; env https_proxy; env no_proxy; diff --git a/pact_broker/Gemfile.lock b/pact_broker/Gemfile.lock index b417b0a..11346fa 100644 --- a/pact_broker/Gemfile.lock +++ b/pact_broker/Gemfile.lock @@ -64,7 +64,7 @@ GEM rspec (>= 2.14) term-ansicolor (~> 1.0) thor - pact_broker (2.20.0) + pact_broker (2.22.0) dry-types (~> 0.10.3) dry-validation (~> 0.10.5) haml (~> 5.0) @@ -93,7 +93,7 @@ GEM rake (>= 0.8.1) pg (1.0.0) rack (2.0.5) - rack-protection (2.0.2) + rack-protection (2.0.3) rack rake (12.3.1) randexp (0.1.7) @@ -122,10 +122,10 @@ GEM rspec-support (3.7.1) semver2 (3.4.2) sequel (5.9.0) - sinatra (2.0.2) + sinatra (2.0.3) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.2) + rack-protection (= 2.0.3) tilt (~> 2.0) sqlite3 (1.3.13) sucker_punch (2.0.4) diff --git a/pact_broker/config.ru b/pact_broker/config.ru index 950fbcf..21d1428 100644 --- a/pact_broker/config.ru +++ b/pact_broker/config.ru @@ -4,15 +4,27 @@ require_relative 'logger' require_relative 'basic_auth' require_relative 'database_connection' require_relative 'passenger_config' +require_relative 'docker_configuration' + +dc = PactBroker::DockerConfiguration.new(ENV, PactBroker::Configuration.default_configuration) +dc.pact_broker_environment_variables.each{ |key, value| $logger.info "#{key}=#{value}"} app = PactBroker::App.new do | config | config.logger = $logger config.database_connection = create_database_connection(config.logger) config.database_connection.timezone = :utc + config.webhook_host_whitelist = dc.webhook_host_whitelist + config.webhook_http_method_whitelist = dc.webhook_http_method_whitelist + config.webhook_scheme_whitelist = dc.webhook_scheme_whitelist + config.base_equality_only_on_content_that_affects_verification_results = dc.base_equality_only_on_content_that_affects_verification_results end PactBroker.configuration.load_from_database! +PactBroker::Configuration::SAVABLE_SETTING_NAMES.each do | setting | + $logger.info "PactBroker.configuration.#{setting}=#{PactBroker.configuration.send(setting).inspect}" +end + basic_auth_username = ENV.fetch('PACT_BROKER_BASIC_AUTH_USERNAME','') basic_auth_password = ENV.fetch('PACT_BROKER_BASIC_AUTH_PASSWORD', '') use_basic_auth = basic_auth_username != '' && basic_auth_password != '' diff --git a/pact_broker/database_connection.rb b/pact_broker/database_connection.rb index 9cca47d..5167f78 100644 --- a/pact_broker/database_connection.rb +++ b/pact_broker/database_connection.rb @@ -4,16 +4,17 @@ def create_database_connection(logger) database_adapter = ENV.fetch('PACT_BROKER_DATABASE_ADAPTER','') != '' ? ENV['PACT_BROKER_DATABASE_ADAPTER'] : 'postgres' - credentials = { + config = { adapter: database_adapter, user: ENV['PACT_BROKER_DATABASE_USERNAME'], password: ENV['PACT_BROKER_DATABASE_PASSWORD'], host: ENV['PACT_BROKER_DATABASE_HOST'], - database: ENV['PACT_BROKER_DATABASE_NAME'] + database: ENV['PACT_BROKER_DATABASE_NAME'], + encoding: 'utf8' } if ENV['PACT_BROKER_DATABASE_PORT'] =~ /^\d+$/ - credentials[:port] = ENV['PACT_BROKER_DATABASE_PORT'].to_i + config[:port] = ENV['PACT_BROKER_DATABASE_PORT'].to_i end ## @@ -33,7 +34,9 @@ def create_database_connection(logger) # -1 means that connections will be validated every time, which avoids errors # when databases are restarted and connections are killed. This has a performance # penalty, so consider increasing this timeout if building a frequently accessed service. - connection = Sequel.connect(credentials.merge(logger: DatabaseLogger.new(logger), encoding: 'utf8')) + logger.info "Connecting to database with config: #{config.merge(password: "*****")}" + config[:logger] = DatabaseLogger.new(logger) + connection = Sequel.connect(config) connection.extension(:connection_validator) connection.pool.connection_validation_timeout = -1 connection diff --git a/pact_broker/docker_configuration.rb b/pact_broker/docker_configuration.rb new file mode 100644 index 0000000..20716e2 --- /dev/null +++ b/pact_broker/docker_configuration.rb @@ -0,0 +1,86 @@ +# @private - do not rely on these classes as a public interface + +module PactBroker + class DockerConfiguration + def initialize env, default_configuration + @env = env + @default_configuration = default_configuration + end + + def pact_broker_environment_variables + @env.each_with_object({}) do | (key, value), hash | + if key.start_with?("PACT_BROKER_") + hash[key] = key =~ /password/i ? "*****" : value + end + end + end + + def webhook_host_whitelist + space_delimited_string_list_or_default(:webhook_host_whitelist) + end + + def webhook_scheme_whitelist + space_delimited_string_list_or_default(:webhook_scheme_whitelist) + end + + def webhook_http_method_whitelist + space_delimited_string_list_or_default(:webhook_http_method_whitelist) + end + + def base_equality_only_on_content_that_affects_verification_results + if env_populated?(:base_equality_only_on_content_that_affects_verification_results) + env(:base_equality_only_on_content_that_affects_verification_results) == 'true' + else + default(:base_equality_only_on_content_that_affects_verification_results) + end + end + + def env name + @env["PACT_BROKER_#{name.to_s.upcase}"] + end + + def env_populated? name + (env(name) || "").size > 0 + end + + def default property_name + @default_configuration.send(property_name) + end + + def space_delimited_string_list_or_default property_name + if env_populated?(property_name) + SpaceDelimitedStringList.parse(env(property_name)) + else + default(property_name) + end + end + + class SpaceDelimitedStringList < Array + + def initialize list + super(list) + end + + def self.parse(string) + array = (string || '').split(' ').collect do | word | + if word[0] == '/' and word[-1] == '/' + Regexp.new(word[1..-2]) + else + word + end + end + SpaceDelimitedStringList.new(array) + end + + def to_s + collect do | word | + if word.is_a?(Regexp) + "/#{word.source}/" + else + word + end + end.join(' ') + end + end + end +end \ No newline at end of file diff --git a/script/test.sh b/script/test.sh index e35d8b3..75ec49c 100755 --- a/script/test.sh +++ b/script/test.sh @@ -60,6 +60,10 @@ fi [ -z "${PSQL_CONT_NAME}" ] && PSQL_CONT_NAME="postgres" [ -z "${PACT_BROKER_DATABASE_ADAPTER}" ] && PACT_BROKER_DATABASE_ADAPTER="postgres" [ -z "${PACT_BROKER_PUBLIC_HEARTBEAT}" ] && PACT_BROKER_PUBLIC_HEARTBEAT="true" +[ -z "${PACT_BROKER_PUBLIC_HEARTBEAT}" ] && PACT_BROKER_PUBLIC_HEARTBEAT="true" +[ -z "${PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST}" ] && PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST="GET POST" +[ -z "${PACT_BROKER_WEBHOOK_SCHEME_WHITELIST}" ] && PACT_BROKER_WEBHOOK_SCHEME_WHITELIST="http https" +[ -z "${PACT_BROKER_WEBHOOK_HOST_WHITELIST}" ] && PACT_BROKER_WEBHOOK_HOST_WHITELIST="/.*\\.foo\\.com$/ bar.com 10.2.3.41/24" echo "Will build the pact broker" docker build -t=dius/pact_broker . @@ -161,6 +165,9 @@ docker run --privileged --name=${PACT_CONT_NAME} -d -p ${PORT_BIND} \ -e PACT_BROKER_BASIC_AUTH_USERNAME=${PACT_BROKER_BASIC_AUTH_USERNAME} \ -e PACT_BROKER_BASIC_AUTH_PASSWORD=${PACT_BROKER_BASIC_AUTH_PASSWORD} \ -e PACT_BROKER_PUBLIC_HEARTBEAT=${PACT_BROKER_PUBLIC_HEARTBEAT} \ + -e PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST="${PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST}" \ + -e PACT_BROKER_WEBHOOK_SCHEME_WHITELIST="${PACT_BROKER_WEBHOOK_SCHEME_WHITELIST}" \ + -e PACT_BROKER_WEBHOOK_HOST_WHITELIST="${PACT_BROKER_WEBHOOK_HOST_WHITELIST}" \ -e PACT_BROKER_LOG_LEVEL=INFO \ dius/pact_broker sleep 1 && docker logs ${PACT_CONT_NAME} diff --git a/spec/docker_configuration_spec.rb b/spec/docker_configuration_spec.rb new file mode 100644 index 0000000..f5c0a91 --- /dev/null +++ b/spec/docker_configuration_spec.rb @@ -0,0 +1,93 @@ +$: << "." +require "pact_broker/docker_configuration" +require 'rspec/its' + +RSpec.describe PactBroker::DockerConfiguration do + + subject { PactBroker::DockerConfiguration.new(env, default_configuration) } + + let(:env) do + { + "PACT_BROKER_WEBHOOK_HOST_WHITELIST" => host_whitelist + } + end + + let(:default_configuration) do + instance_double('default configuration', + webhook_host_whitelist: 'default' + ) + end + + describe "pact_broker_environment_variables" do + let(:env) do + { + "PACT_BROKER_FOO" => "foo", + "PACT_BROKER_PASSWORD" => "bar", + "SOMETHING" => "foo" + } + end + + let(:expected_environment_variables) do + { + "PACT_BROKER_FOO" => "foo", + "PACT_BROKER_PASSWORD" => "*****" + } + end + + its(:pact_broker_environment_variables) { is_expected.to eq expected_environment_variables } + end + + describe "webhook_host_whitelist" do + context "when PACT_BROKER_WEBHOOK_HOST_WHITELIST is 'foo bar'" do + let(:host_whitelist) { "foo bar" } + its(:webhook_host_whitelist) { is_expected.to eq ["foo", "bar"] } + end + + context "when PACT_BROKER_WEBHOOK_HOST_WHITELIST is ''" do + let(:host_whitelist) { "" } + its(:webhook_host_whitelist) { is_expected.to eq 'default' } + end + end +end + +class PactBroker::DockerConfiguration + describe SpaceDelimitedStringList do + describe "parse" do + subject { SpaceDelimitedStringList.parse(input) } + + context "when input is ''" do + let(:input) { "" } + + it { is_expected.to eq [] } + + its(:to_s) { is_expected.to eq input } + end + + context "when input is 'foo bar'" do + let(:input) { "foo bar" } + + it { is_expected.to eq ["foo", "bar"] } + + it { is_expected.to be_a SpaceDelimitedStringList } + + its(:to_s) { is_expected.to eq input } + end + + context "when input is '/foo.*/'" do + let(:input) { "/foo.*/" } + + it { is_expected.to eq [/foo.*/] } + + its(:to_s) { is_expected.to eq input } + end + + context "when input is '/foo\\.*/' (note double backslash)" do + let(:input) { "/foo\\.*/" } + + it { is_expected.to eq [/foo\.*/] } + + its(:to_s) { is_expected.to eq input } + end + end + end +end