Skip to content

Commit

Permalink
feat: allow read only credentials to be set via environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
bethesque committed Aug 9, 2018
1 parent 889d5e0 commit 49bf6ce
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pact_broker/pact_broker.sqlite
pact_broker.sqlite
pact_broker/log
pact_broker/tmp
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ gem 'rake', '~> 12.0'
gem 'conventional-changelog', '~>1.3'
gem 'rspec', '~> 3.7'
gem 'rspec-its', '~> 1.2'
gem 'rack-test'
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ GEM
specs:
conventional-changelog (1.3.0)
diff-lcs (1.3)
rack (2.0.5)
rack-test (1.0.0)
rack (>= 1.0, < 3)
rake (12.3.0)
rspec (3.7.0)
rspec-core (~> 3.7.0)
Expand All @@ -26,6 +29,7 @@ PLATFORMS

DEPENDENCIES
conventional-changelog (~> 1.3)
rack-test
rake (~> 12.0)
rspec (~> 3.7)
rspec-its (~> 1.2)
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,24 @@ For an sqlite database (only recommended for investigation/spikes, as it will be
* Apart from creating a database no further preparation is required.

## Using basic auth
Run your container with `PACT_BROKER_BASIC_AUTH_USERNAME` and `PACT_BROKER_BASIC_AUTH_PASSWORD` set to enable basic auth for the pact broker application. Note that the [verification status badges][badges] are not protected by basic auth, so that you may embed them in README markdown.

If you are using the docker container within an AWS autoscaling group, and you need to make a heartbeat URL publicly available, set `PACT_BROKER_PUBLIC_HEARTBEAT=true`.
To enable basic auth, run your container with:

* `PACT_BROKER_BASIC_AUTH_USERNAME`
* `PACT_BROKER_BASIC_AUTH_PASSWORD`
* `PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME`
* `PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD`

Developers should use the read only credentials on their local machines, and the CI should use the read/write credentials. This will ensure that pacts and verification results are only published from your CI.

Note that the [verification status badges][badges] are not protected by basic auth, so that you may embed them in README markdown.

## Heartbeat URL

If you are using the docker container within an AWS autoscaling group, and you need to make a heartbeat URL publicly available, set `PACT_BROKER_PUBLIC_HEARTBEAT=true`. No database connection will be made during the execution of this endpoint.

## Using SSL

See the [Pact Broker configuration documentation][reverse-proxy].

## Setting the log level
Expand Down
2 changes: 2 additions & 0 deletions container/etc/nginx/main.d/pactbroker-env.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ env PACT_BROKER_DATABASE_NAME;
env PACT_BROKER_DATABASE_PORT;
env PACT_BROKER_BASIC_AUTH_USERNAME;
env PACT_BROKER_BASIC_AUTH_PASSWORD;
env PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME;
env PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD;
env PACT_BROKER_PUBLIC_HEARTBEAT;
env PACT_BROKER_LOG_LEVEL;
env PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST;
Expand Down
37 changes: 28 additions & 9 deletions pact_broker/basic_auth.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
class BasicAuth
PATH_INFO = 'PATH_INFO'.freeze
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
GET = 'GET'.freeze
OPTIONS = 'OPTIONS'.freeze
HEAD = 'HEAD'.freeze
BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze
HEARTBEAT_PATH = "/diagnostic/status/heartbeat".freeze

def initialize(app, username, password, allow_public_access_to_heartbeat)
def initialize(app, write_user_username, write_user_password, read_user_username, read_user_password, allow_public_access_to_heartbeat)
@app = app
@expected_username = username
@expected_password = password
@write_user_username = write_user_username
@write_user_password = write_user_password
@read_user_username = read_user_username
@read_user_password = read_user_password
@allow_public_access_to_heartbeat = allow_public_access_to_heartbeat

@app_with_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
username == @expected_username && password == @expected_password
@app_with_write_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
username == @write_user_username && password == @write_user_password
end

@app_with_read_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
(username == @write_user_username && password == @write_user_password) ||
(username == @read_user_username && password == @read_user_password)
end
end

def call(env)
if use_basic_auth? env
@app_with_auth.call(env)
if read_request?(env)
@app_with_read_auth.call(env)
else
@app_with_write_auth.call(env)
end
else
@app.call(env)
end
end

def read_request?(env)
env.fetch(REQUEST_METHOD) == GET || env.fetch(REQUEST_METHOD) == OPTIONS || env.fetch(REQUEST_METHOD) == HEAD
end

def use_basic_auth?(env)
!(is_badge_path?(env) || is_heartbeat_and_public_access_allowed?(env))
!allow_public_access(env)
end

def is_badge_path?(env)
env[PATH_INFO] =~ BADGE_PATH
def allow_public_access(env)
env[PATH_INFO] =~ BADGE_PATH || is_heartbeat_and_public_access_allowed?(env)
end

def is_heartbeat_and_public_access_allowed?(env)
Expand Down
10 changes: 9 additions & 1 deletion pact_broker/config.ru
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,19 @@ end

basic_auth_username = ENV.fetch('PACT_BROKER_BASIC_AUTH_USERNAME','')
basic_auth_password = ENV.fetch('PACT_BROKER_BASIC_AUTH_PASSWORD', '')
basic_auth_read_only_username = ENV.fetch('PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME','')
basic_auth_read_only_password = ENV.fetch('PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD', '')
use_basic_auth = basic_auth_username != '' && basic_auth_password != ''
allow_public_access_to_heartbeat = ENV.fetch('PACT_BROKER_PUBLIC_HEARTBEAT', '') == 'true'


if use_basic_auth
app = BasicAuth.new(app, basic_auth_username, basic_auth_password, allow_public_access_to_heartbeat)
use BasicAuth,
basic_auth_username,
basic_auth_password,
basic_auth_read_only_username,
basic_auth_read_only_password,
allow_public_access_to_heartbeat
end

run app
6 changes: 6 additions & 0 deletions script/dev/env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export PACT_BROKER_BASIC_AUTH_USERNAME=foo
export PACT_BROKER_BASIC_AUTH_PASSWORD=bar
export PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME=fooro
export PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD=barro
export PACT_BROKER_DATABASE_ADAPTER=sqlite
export PACT_BROKER_DATABASE_NAME=tmp/pact_broker.sqlite3
2 changes: 2 additions & 0 deletions script/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ fi
[ -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"

bundle exec rspec spec

echo "Will build the pact broker"
docker build -t=dius/pact_broker .

Expand Down
194 changes: 194 additions & 0 deletions spec/basic_auth_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
require_relative "../pact_broker/basic_auth"
require "rack/test"

RSpec.describe "basic auth" do

include Rack::Test::Methods

let(:protected_app) { ->(env) { [200, {}, []]} }

let(:app) { BasicAuth.new(protected_app, 'write_username', 'write_password', 'read_username', 'read_password', allow_public_access_to_heartbeat) }
let(:allow_public_access_to_heartbeat) { true }


context "when requesting the heartbeat" do
let(:path) { "/diagnostic/status/heartbeat" }

context "when allow_public_access_to_heartbeat is true" do
context "when no credentials are used" do
it "allows GET" do
get path
expect(last_response.status).to eq 200
end
end
end

context "when allow_public_access_to_heartbeat is false" do
let(:allow_public_access_to_heartbeat) { false }

context "when no credentials are used" do
it "does not allow GET" do
get path
expect(last_response.status).to eq 401
end
end

context "when the correct credentials are used" do
it "allows GET" do
basic_authorize 'read_username', 'read_password'
get path
expect(last_response.status).to eq 200
end
end
end
end

context "when requesting a badge" do
context "when no credentials are used" do
it "allows GET" do
get "pacts/provider/foo/consumer/bar/badge"
expect(last_response.status).to eq 200
end
end
end

context "with the correct username and password for the write user" do
it "allows GET" do
basic_authorize 'write_username', 'write_password'
get "/"
expect(last_response.status).to eq 200
end

it "allows POST" do
basic_authorize 'write_username', 'write_password'
post "/"
expect(last_response.status).to eq 200
end

it "allows HEAD" do
basic_authorize 'write_username', 'write_password'
head "/"
expect(last_response.status).to eq 200
end

it "allows OPTIONS" do
basic_authorize 'write_username', 'write_password'
options "/"
expect(last_response.status).to eq 200
end

it "allows PUT" do
basic_authorize 'write_username', 'write_password'
delete "/"
expect(last_response.status).to eq 200
end

it "allows PATCH" do
basic_authorize 'write_username', 'write_password'
patch "/"
expect(last_response.status).to eq 200
end

it "allows DELETE" do
basic_authorize 'write_username', 'write_password'
delete "/"
expect(last_response.status).to eq 200
end
end

context "with the incorrect username and password for the write user" do
it "does not allow POST" do
basic_authorize 'foo', 'password'
post "/"
expect(last_response.status).to eq 401
end
end

context "with the correct username and password for the read user" do
it "allows GET" do
basic_authorize 'read_username', 'read_password'
get "/"
expect(last_response.status).to eq 200
end

it "allows OPTIONS" do
basic_authorize 'read_username', 'read_password'
options "/"
expect(last_response.status).to eq 200
end

it "allows HEAD" do
basic_authorize 'read_username', 'read_password'
head "/"
expect(last_response.status).to eq 200
end

it "does not allow POST" do
basic_authorize 'read_username', 'read_password'
post "/"
expect(last_response.status).to eq 401
end

it "does not allow PUT" do
basic_authorize 'read_username', 'read_password'
put "/"
expect(last_response.status).to eq 401
end

it "does not allow PATCH" do
basic_authorize 'read_username', 'read_password'
patch "/"
expect(last_response.status).to eq 401
end

it "does not allow DELETE" do
basic_authorize 'read_username', 'read_password'
delete "/"
expect(last_response.status).to eq 401
end
end

context "with the incorrect username and password for the write user" do
it "does not allow GET" do
basic_authorize 'write_username', 'wrongpassword'
get "/"
expect(last_response.status).to eq 401
end
end

context "with the incorrect username and password for the read user" do
it "does not allow GET" do
basic_authorize 'read_username', 'wrongpassword'
get "/"
expect(last_response.status).to eq 401
end
end

context "with a request to the badge URL" do
context "with no credentials" do
it "allows GET" do
get "/pacts/provider/foo/consumer/bar/badge"
expect(last_response.status).to eq 200
end
end
end

context "when there is no read only user configured" do
let(:app) { BasicAuth.new(protected_app, 'write_username', 'write_password', nil, nil, allow_public_access_to_heartbeat) }

context "with no credentials" do
it "does not allow GET" do
get "/"
expect(last_response.status).to eq 401
end
end

context "with credentials" do
it "does not allow GET" do
basic_authorize "foo", "bar"
get "/"
expect(last_response.status).to eq 401
end
end
end
end

0 comments on commit 49bf6ce

Please sign in to comment.