Skip to content

Commit 49bf6ce

Browse files
committed
feat: allow read only credentials to be set via environment variables
1 parent 889d5e0 commit 49bf6ce

File tree

10 files changed

+262
-12
lines changed

10 files changed

+262
-12
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pact_broker/pact_broker.sqlite
22
pact_broker.sqlite
33
pact_broker/log
4+
pact_broker/tmp

Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ gem 'rake', '~> 12.0'
44
gem 'conventional-changelog', '~>1.3'
55
gem 'rspec', '~> 3.7'
66
gem 'rspec-its', '~> 1.2'
7+
gem 'rack-test'

Gemfile.lock

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ GEM
33
specs:
44
conventional-changelog (1.3.0)
55
diff-lcs (1.3)
6+
rack (2.0.5)
7+
rack-test (1.0.0)
8+
rack (>= 1.0, < 3)
69
rake (12.3.0)
710
rspec (3.7.0)
811
rspec-core (~> 3.7.0)
@@ -26,6 +29,7 @@ PLATFORMS
2629

2730
DEPENDENCIES
2831
conventional-changelog (~> 1.3)
32+
rack-test
2933
rake (~> 12.0)
3034
rspec (~> 3.7)
3135
rspec-its (~> 1.2)

README.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,24 @@ For an sqlite database (only recommended for investigation/spikes, as it will be
3838
* Apart from creating a database no further preparation is required.
3939

4040
## Using basic auth
41-
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.
4241

43-
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`.
42+
To enable basic auth, run your container with:
43+
44+
* `PACT_BROKER_BASIC_AUTH_USERNAME`
45+
* `PACT_BROKER_BASIC_AUTH_PASSWORD`
46+
* `PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME`
47+
* `PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD`
48+
49+
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.
50+
51+
Note that the [verification status badges][badges] are not protected by basic auth, so that you may embed them in README markdown.
52+
53+
## Heartbeat URL
54+
55+
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.
4456

4557
## Using SSL
58+
4659
See the [Pact Broker configuration documentation][reverse-proxy].
4760

4861
## Setting the log level

container/etc/nginx/main.d/pactbroker-env.conf

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ env PACT_BROKER_DATABASE_NAME;
66
env PACT_BROKER_DATABASE_PORT;
77
env PACT_BROKER_BASIC_AUTH_USERNAME;
88
env PACT_BROKER_BASIC_AUTH_PASSWORD;
9+
env PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME;
10+
env PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD;
911
env PACT_BROKER_PUBLIC_HEARTBEAT;
1012
env PACT_BROKER_LOG_LEVEL;
1113
env PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST;

pact_broker/basic_auth.rb

+28-9
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,52 @@
11
class BasicAuth
22
PATH_INFO = 'PATH_INFO'.freeze
3+
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
4+
GET = 'GET'.freeze
5+
OPTIONS = 'OPTIONS'.freeze
6+
HEAD = 'HEAD'.freeze
37
BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze
48
HEARTBEAT_PATH = "/diagnostic/status/heartbeat".freeze
59

6-
def initialize(app, username, password, allow_public_access_to_heartbeat)
10+
def initialize(app, write_user_username, write_user_password, read_user_username, read_user_password, allow_public_access_to_heartbeat)
711
@app = app
8-
@expected_username = username
9-
@expected_password = password
12+
@write_user_username = write_user_username
13+
@write_user_password = write_user_password
14+
@read_user_username = read_user_username
15+
@read_user_password = read_user_password
1016
@allow_public_access_to_heartbeat = allow_public_access_to_heartbeat
1117

12-
@app_with_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
13-
username == @expected_username && password == @expected_password
18+
@app_with_write_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
19+
username == @write_user_username && password == @write_user_password
20+
end
21+
22+
@app_with_read_auth = Rack::Auth::Basic.new(app, "Restricted area") do |username, password|
23+
(username == @write_user_username && password == @write_user_password) ||
24+
(username == @read_user_username && password == @read_user_password)
1425
end
1526
end
1627

1728
def call(env)
1829
if use_basic_auth? env
19-
@app_with_auth.call(env)
30+
if read_request?(env)
31+
@app_with_read_auth.call(env)
32+
else
33+
@app_with_write_auth.call(env)
34+
end
2035
else
2136
@app.call(env)
2237
end
2338
end
2439

40+
def read_request?(env)
41+
env.fetch(REQUEST_METHOD) == GET || env.fetch(REQUEST_METHOD) == OPTIONS || env.fetch(REQUEST_METHOD) == HEAD
42+
end
43+
2544
def use_basic_auth?(env)
26-
!(is_badge_path?(env) || is_heartbeat_and_public_access_allowed?(env))
45+
!allow_public_access(env)
2746
end
2847

29-
def is_badge_path?(env)
30-
env[PATH_INFO] =~ BADGE_PATH
48+
def allow_public_access(env)
49+
env[PATH_INFO] =~ BADGE_PATH || is_heartbeat_and_public_access_allowed?(env)
3150
end
3251

3352
def is_heartbeat_and_public_access_allowed?(env)

pact_broker/config.ru

+9-1
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,19 @@ end
2828

2929
basic_auth_username = ENV.fetch('PACT_BROKER_BASIC_AUTH_USERNAME','')
3030
basic_auth_password = ENV.fetch('PACT_BROKER_BASIC_AUTH_PASSWORD', '')
31+
basic_auth_read_only_username = ENV.fetch('PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME','')
32+
basic_auth_read_only_password = ENV.fetch('PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD', '')
3133
use_basic_auth = basic_auth_username != '' && basic_auth_password != ''
3234
allow_public_access_to_heartbeat = ENV.fetch('PACT_BROKER_PUBLIC_HEARTBEAT', '') == 'true'
3335

36+
3437
if use_basic_auth
35-
app = BasicAuth.new(app, basic_auth_username, basic_auth_password, allow_public_access_to_heartbeat)
38+
use BasicAuth,
39+
basic_auth_username,
40+
basic_auth_password,
41+
basic_auth_read_only_username,
42+
basic_auth_read_only_password,
43+
allow_public_access_to_heartbeat
3644
end
3745

3846
run app

script/dev/env.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export PACT_BROKER_BASIC_AUTH_USERNAME=foo
2+
export PACT_BROKER_BASIC_AUTH_PASSWORD=bar
3+
export PACT_BROKER_BASIC_AUTH_READ_ONLY_USERNAME=fooro
4+
export PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD=barro
5+
export PACT_BROKER_DATABASE_ADAPTER=sqlite
6+
export PACT_BROKER_DATABASE_NAME=tmp/pact_broker.sqlite3

script/test.sh

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ fi
6565
[ -z "${PACT_BROKER_WEBHOOK_SCHEME_WHITELIST}" ] && PACT_BROKER_WEBHOOK_SCHEME_WHITELIST="http https"
6666
[ -z "${PACT_BROKER_WEBHOOK_HOST_WHITELIST}" ] && PACT_BROKER_WEBHOOK_HOST_WHITELIST="/.*\\.foo\\.com$/ bar.com 10.2.3.41/24"
6767

68+
bundle exec rspec spec
69+
6870
echo "Will build the pact broker"
6971
docker build -t=dius/pact_broker .
7072

spec/basic_auth_spec.rb

+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
require_relative "../pact_broker/basic_auth"
2+
require "rack/test"
3+
4+
RSpec.describe "basic auth" do
5+
6+
include Rack::Test::Methods
7+
8+
let(:protected_app) { ->(env) { [200, {}, []]} }
9+
10+
let(:app) { BasicAuth.new(protected_app, 'write_username', 'write_password', 'read_username', 'read_password', allow_public_access_to_heartbeat) }
11+
let(:allow_public_access_to_heartbeat) { true }
12+
13+
14+
context "when requesting the heartbeat" do
15+
let(:path) { "/diagnostic/status/heartbeat" }
16+
17+
context "when allow_public_access_to_heartbeat is true" do
18+
context "when no credentials are used" do
19+
it "allows GET" do
20+
get path
21+
expect(last_response.status).to eq 200
22+
end
23+
end
24+
end
25+
26+
context "when allow_public_access_to_heartbeat is false" do
27+
let(:allow_public_access_to_heartbeat) { false }
28+
29+
context "when no credentials are used" do
30+
it "does not allow GET" do
31+
get path
32+
expect(last_response.status).to eq 401
33+
end
34+
end
35+
36+
context "when the correct credentials are used" do
37+
it "allows GET" do
38+
basic_authorize 'read_username', 'read_password'
39+
get path
40+
expect(last_response.status).to eq 200
41+
end
42+
end
43+
end
44+
end
45+
46+
context "when requesting a badge" do
47+
context "when no credentials are used" do
48+
it "allows GET" do
49+
get "pacts/provider/foo/consumer/bar/badge"
50+
expect(last_response.status).to eq 200
51+
end
52+
end
53+
end
54+
55+
context "with the correct username and password for the write user" do
56+
it "allows GET" do
57+
basic_authorize 'write_username', 'write_password'
58+
get "/"
59+
expect(last_response.status).to eq 200
60+
end
61+
62+
it "allows POST" do
63+
basic_authorize 'write_username', 'write_password'
64+
post "/"
65+
expect(last_response.status).to eq 200
66+
end
67+
68+
it "allows HEAD" do
69+
basic_authorize 'write_username', 'write_password'
70+
head "/"
71+
expect(last_response.status).to eq 200
72+
end
73+
74+
it "allows OPTIONS" do
75+
basic_authorize 'write_username', 'write_password'
76+
options "/"
77+
expect(last_response.status).to eq 200
78+
end
79+
80+
it "allows PUT" do
81+
basic_authorize 'write_username', 'write_password'
82+
delete "/"
83+
expect(last_response.status).to eq 200
84+
end
85+
86+
it "allows PATCH" do
87+
basic_authorize 'write_username', 'write_password'
88+
patch "/"
89+
expect(last_response.status).to eq 200
90+
end
91+
92+
it "allows DELETE" do
93+
basic_authorize 'write_username', 'write_password'
94+
delete "/"
95+
expect(last_response.status).to eq 200
96+
end
97+
end
98+
99+
context "with the incorrect username and password for the write user" do
100+
it "does not allow POST" do
101+
basic_authorize 'foo', 'password'
102+
post "/"
103+
expect(last_response.status).to eq 401
104+
end
105+
end
106+
107+
context "with the correct username and password for the read user" do
108+
it "allows GET" do
109+
basic_authorize 'read_username', 'read_password'
110+
get "/"
111+
expect(last_response.status).to eq 200
112+
end
113+
114+
it "allows OPTIONS" do
115+
basic_authorize 'read_username', 'read_password'
116+
options "/"
117+
expect(last_response.status).to eq 200
118+
end
119+
120+
it "allows HEAD" do
121+
basic_authorize 'read_username', 'read_password'
122+
head "/"
123+
expect(last_response.status).to eq 200
124+
end
125+
126+
it "does not allow POST" do
127+
basic_authorize 'read_username', 'read_password'
128+
post "/"
129+
expect(last_response.status).to eq 401
130+
end
131+
132+
it "does not allow PUT" do
133+
basic_authorize 'read_username', 'read_password'
134+
put "/"
135+
expect(last_response.status).to eq 401
136+
end
137+
138+
it "does not allow PATCH" do
139+
basic_authorize 'read_username', 'read_password'
140+
patch "/"
141+
expect(last_response.status).to eq 401
142+
end
143+
144+
it "does not allow DELETE" do
145+
basic_authorize 'read_username', 'read_password'
146+
delete "/"
147+
expect(last_response.status).to eq 401
148+
end
149+
end
150+
151+
context "with the incorrect username and password for the write user" do
152+
it "does not allow GET" do
153+
basic_authorize 'write_username', 'wrongpassword'
154+
get "/"
155+
expect(last_response.status).to eq 401
156+
end
157+
end
158+
159+
context "with the incorrect username and password for the read user" do
160+
it "does not allow GET" do
161+
basic_authorize 'read_username', 'wrongpassword'
162+
get "/"
163+
expect(last_response.status).to eq 401
164+
end
165+
end
166+
167+
context "with a request to the badge URL" do
168+
context "with no credentials" do
169+
it "allows GET" do
170+
get "/pacts/provider/foo/consumer/bar/badge"
171+
expect(last_response.status).to eq 200
172+
end
173+
end
174+
end
175+
176+
context "when there is no read only user configured" do
177+
let(:app) { BasicAuth.new(protected_app, 'write_username', 'write_password', nil, nil, allow_public_access_to_heartbeat) }
178+
179+
context "with no credentials" do
180+
it "does not allow GET" do
181+
get "/"
182+
expect(last_response.status).to eq 401
183+
end
184+
end
185+
186+
context "with credentials" do
187+
it "does not allow GET" do
188+
basic_authorize "foo", "bar"
189+
get "/"
190+
expect(last_response.status).to eq 401
191+
end
192+
end
193+
end
194+
end

0 commit comments

Comments
 (0)