Skip to content

Commit 0e75eb7

Browse files
authored
Add bypass of herokuapp access restriction (#85)
Refs teamniteo/ops#2359
1 parent 6dac0ed commit 0e75eb7

File tree

3 files changed

+73
-3
lines changed

3 files changed

+73
-3
lines changed

README.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ Usage example for tweens::
4747

4848
The ``pyramid_heroku.herokuapp_access`` tween depends on
4949
``pyramid_heroku.client_addr`` tween and it requires you to list allowlisted IPs
50-
in the ``pyramid_heroku.herokuapp_allowlist`` setting.
50+
in the ``pyramid_heroku.herokuapp_allowlist`` setting. A bypass is possible
51+
by setting the `HEROKUAPP_ACCESS_BYPASS` environment variable to a secret value
52+
and then sending a request with the `HEROKUAPP_ACCESS_BYPASS` header set to the
53+
same secret value.
5154

5255
The ``pyramid_heroku.client_addr`` tween sets request.client_addr to an IP we
5356
can trust. It handles IP spoofing via ``X-Forwarded-For`` headers and

pyramid_heroku/herokuapp_access.py

+24-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
from pyramid.response import Response
44

55
import logging
6+
import os
67

78

89
def includeme(config):
9-
config.add_tween("pyramid_heroku.herokuapp_access.HerokuappAccess")
10+
config.add_tween(
11+
"pyramid_heroku.herokuapp_access.HerokuappAccess",
12+
under="pyramid_heroku.client_addr.ClientAddr",
13+
)
1014

1115

1216
class HerokuappAccess(object):
@@ -20,6 +24,8 @@ class HerokuappAccess(object):
2024
tween.
2125
"""
2226

27+
import os
28+
2329
def __init__(self, handler, registry):
2430
self.handler = handler
2531
self.registry = registry
@@ -30,6 +36,23 @@ def __call__(self, request):
3036
"pyramid_heroku.herokuapp_allowlist", ""
3137
).split("\n")
3238

39+
if os.environ.get("HEROKUAPP_ACCESS_BYPASS"):
40+
if request.headers.get("HEROKUAPP_ACCESS_BYPASS") == os.environ.get(
41+
"HEROKUAPP_ACCESS_BYPASS"
42+
):
43+
if request.registry.settings.get("pyramid_heroku.structlog"):
44+
import structlog
45+
46+
logger = structlog.getLogger(__name__)
47+
logger.info(
48+
"Herokuapp access bypassed", user_ip=request.client_addr
49+
)
50+
else:
51+
logger = logging.getLogger(__name__)
52+
logger.info(f"Herokuapp access bypassed by {request.client_addr}")
53+
54+
return self.handler(request)
55+
3356
if (
3457
"herokuapp.com" in request.headers["Host"]
3558
and request.client_addr not in allowlisted_ips

pyramid_heroku/tests/test_herokuapp_access.py

+45-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import logging
77
import mock
8+
import os
89
import structlog
910
import unittest
1011

@@ -66,7 +67,7 @@ def test_non_allowlisted_ip(self):
6667
assert not self.handler.called, "handler should not be called"
6768
self.assertEqual(len(tweens_handler.records), 1)
6869
self.assertEqual(
69-
"Denied Herokuapp access for Host foo.herokuapp.com and IP 6.6.6.6", # noqa
70+
"Denied Herokuapp access for Host foo.herokuapp.com and IP 6.6.6.6",
7071
tweens_handler.records[0].msg,
7172
)
7273
self.assertEqual(response.status_code, 403)
@@ -101,3 +102,46 @@ def test_herokuapp_allowlist_empty(self):
101102

102103
HerokuappAccess(self.handler, self.request.registry)(self.request)
103104
assert not self.handler.called, "handler should not be called"
105+
106+
@mock.patch.dict(os.environ, {"HEROKUAPP_ACCESS_BYPASS": "foo"})
107+
def test_herokuapp_access_bypass(self):
108+
"The IP check can be bypassed by setting a correct header."
109+
from pyramid_heroku.herokuapp_access import HerokuappAccess
110+
111+
self.request.client_addr = "6.6.6.6"
112+
self.request.headers = {
113+
"Host": "foo.herokuapp.com",
114+
"HEROKUAPP_ACCESS_BYPASS": "foo",
115+
}
116+
117+
# structlog version
118+
HerokuappAccess(self.handler, self.request.registry)(self.request)
119+
self.handler.assert_called_with(self.request)
120+
self.assertEqual(len(tweens_handler.records), 1)
121+
self.assertEqual("Herokuapp access bypassed", tweens_handler.records[0].msg)
122+
123+
# standard logging version
124+
self.request.registry.settings["pyramid_heroku.structlog"] = False
125+
tweens_handler.clear()
126+
HerokuappAccess(self.handler, self.request.registry)(self.request)
127+
self.handler.assert_called_with(self.request)
128+
self.assertEqual(len(tweens_handler.records), 1)
129+
self.assertEqual(
130+
"Herokuapp access bypassed by 6.6.6.6",
131+
tweens_handler.records[0].msg,
132+
)
133+
134+
@mock.patch.dict(os.environ, {"HEROKUAPP_ACCESS_BYPASS": "foo"})
135+
def test_herokuapp_access_bypass_invalid(self):
136+
"Invalid bypass code is rejected."
137+
from pyramid_heroku.herokuapp_access import HerokuappAccess
138+
139+
self.request.client_addr = "6.6.6.6"
140+
self.request.headers = {
141+
"Host": "foo.herokuapp.com",
142+
"HEROKUAPP_ACCESS_BYPASS": "bar",
143+
}
144+
self.request.registry.settings = {}
145+
146+
HerokuappAccess(self.handler, self.request.registry)(self.request)
147+
assert not self.handler.called, "handler should not be called"

0 commit comments

Comments
 (0)