Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
17 changes: 9 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import and configure the library with your Castle API secret.

.. code:: python

from castle.configuration import configuration, WHITELISTED
from castle.configuration import configuration, DEFAULT_ALLOWLIST

# Same as setting it through Castle.api_secret
configuration.api_secret = ':YOUR-API-SECRET'
Expand All @@ -32,16 +32,17 @@ import and configure the library with your Castle API secret.
# Castle::RequestError is raised when timing out in milliseconds (default: 500 milliseconds)
configuration.request_timeout = 1000

# Whitelisted and Blacklisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
# Allowlisted and Denylisted headers are case insensitive
# and allow to use _ and - as a separator, http prefixes are removed
# By default all headers are passed, but some are automatically scrubbed.
# If you need to apply a whitelist, we recommend using the minimum set of
# standard headers that we've exposed in the `WHITELISTED` constant.
# Whitelisted headers
configuration.whitelisted = WHITELISTED + ['X_HEADER']
# If you need to apply an allowlist, we recommend using the minimum set of
# standard headers that we've exposed in the `DEFAULT_ALLOWLIST` constant.
# Allowlisted headers
configuration.allowlisted = DEFAULT_ALLOWLIST + ['X_HEADER']

# Blacklisted headers take advantage over whitelisted elements. Note that
# Denylisted headers take advantage over allowlisted elements. Note that
# some headers are always scrubbed, for security reasons.
configuration.blacklisted = ['HTTP-X-header']
configuration.denylisted = ['HTTP-X-header']

# Castle needs the original IP of the client, not the IP of your proxy or load balancer.
# The SDK will only trust the proxy chain as defined in the configuration.
Expand Down
37 changes: 21 additions & 16 deletions castle/configuration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from castle.exceptions import ConfigurationError
from castle.headers_formatter import HeadersFormatter

DEFAULT_WHITELIST = [
DEFAULT_ALLOWLIST = [
"Accept",
"Accept-Charset",
"Accept-Datetime",
Expand All @@ -11,11 +11,15 @@
"Connection",
"Content-Length",
"Content-Type",
"Cookie",
"Dnt",
"Host",
"Origin",
"Pragma",
"Referer",
"Sec-Fetch-Dest",
"Sec-Fetch-Mode",
"Sec-Fetch-Site",
"Sec-Fetch-User",
"TE",
"Upgrade-Insecure-Requests",
"User-Agent",
Expand Down Expand Up @@ -44,8 +48,8 @@ def __init__(self):
self.host = HOST
self.port = PORT
self.url_prefix = URL_PREFIX
self.whitelisted = []
self.blacklisted = []
self.allowlisted = []
self.denylisted = []
self.request_timeout = REQUEST_TIMEOUT
self.failover_strategy = FAILOVER_STRATEGY
self.ip_headers = []
Expand Down Expand Up @@ -89,26 +93,26 @@ def url_prefix(self, value):
self.__url_prefix = value

@property
def whitelisted(self):
return self.__whitelisted
def allowlisted(self):
return self.__allowlisted

@whitelisted.setter
def whitelisted(self, value):
@allowlisted.setter
def allowlisted(self, value):
if value:
self.__whitelisted = [HeadersFormatter.call(v) for v in value]
self.__allowlisted = [HeadersFormatter.call(v) for v in value]
else:
self.__whitelisted = []
self.__allowlisted = []

@property
def blacklisted(self):
return self.__blacklisted
def denylisted(self):
return self.__denylisted

@blacklisted.setter
def blacklisted(self, value):
@denylisted.setter
def denylisted(self, value):
if value:
self.__blacklisted = [HeadersFormatter.call(v) for v in value]
self.__denylisted = [HeadersFormatter.call(v) for v in value]
else:
self.__blacklisted = []
self.__denylisted = []

@property
def request_timeout(self):
Expand Down Expand Up @@ -173,5 +177,6 @@ def trusted_proxy_depth(self, value):
else:
raise ConfigurationError


# pylint: disable=invalid-name
configuration = Configuration()
14 changes: 7 additions & 7 deletions castle/extractors/headers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from castle.configuration import configuration

ALWAYS_BLACKLISTED = ['Cookie', 'Authorization']
ALWAYS_WHITELISTED = ['User-Agent']
ALWAYS_DENYLISTED = ['Cookie', 'Authorization']
ALWAYS_ALLOWLISTED = ['User-Agent']


class ExtractorsHeaders(object):
def __init__(self, headers):
self.headers = headers
self.no_whitelist = len(configuration.whitelisted) == 0
self.no_whitelist = len(configuration.allowlisted) == 0

def call(self):
result = dict()
Expand All @@ -18,13 +18,13 @@ def call(self):
return result

def _header_value(self, name, value):
if name in ALWAYS_BLACKLISTED:
if name in ALWAYS_DENYLISTED:
return True
if name in ALWAYS_WHITELISTED:
if name in ALWAYS_ALLOWLISTED:
return value
if name in configuration.blacklisted:
if name in configuration.denylisted:
return True
if self.no_whitelist or (name in configuration.whitelisted):
if self.no_whitelist or (name in configuration.allowlisted):
return value

return True
1 change: 1 addition & 0 deletions castle/extractors/ip.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# list of header which are used with proxy depth setting
DEPTH_RELATED = ['X-Forwarded-For']


class ExtractorsIp(object):
def __init__(self, headers):
self.headers = headers
Expand Down
40 changes: 20 additions & 20 deletions castle/test/configuration_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ def test_default_values(self):
self.assertEqual(config.host, 'api.castle.io')
self.assertEqual(config.port, 443)
self.assertEqual(config.url_prefix, '/v1')
self.assertEqual(config.whitelisted, [])
self.assertEqual(config.blacklisted, [])
self.assertEqual(config.allowlisted, [])
self.assertEqual(config.denylisted, [])
self.assertEqual(config.request_timeout, 500)
self.assertEqual(config.failover_strategy, 'allow')
self.assertEqual(config.ip_headers, [])
Expand All @@ -37,35 +37,35 @@ def test_url_prefix_setter(self):
config.url_prefix = '/v2'
self.assertEqual(config.url_prefix, '/v2')

def test_whitelisted_setter_list(self):
def test_allowlisted_setter_list(self):
config = Configuration()
config.whitelisted = ['test']
self.assertEqual(config.whitelisted, ['Test'])
config.allowlisted = ['test']
self.assertEqual(config.allowlisted, ['Test'])

def test_whitelisted_setter_none(self):
def test_allowlisted_setter_none(self):
config = Configuration()
config.whitelisted = None
self.assertEqual(config.whitelisted, [])
config.allowlisted = None
self.assertEqual(config.allowlisted, [])

def test_whitelisted_setter_empty(self):
def test_allowlisted_setter_empty(self):
config = Configuration()
config.whitelisted = ''
self.assertEqual(config.whitelisted, [])
config.allowlisted = ''
self.assertEqual(config.allowlisted, [])

def test_blacklisted_setter_list(self):
def test_denylisted_setter_list(self):
config = Configuration()
config.blacklisted = ['test']
self.assertEqual(config.blacklisted, ['Test'])
config.denylisted = ['test']
self.assertEqual(config.denylisted, ['Test'])

def test_blacklisted_setter_none(self):
def test_denylisted_setter_none(self):
config = Configuration()
config.blacklisted = None
self.assertEqual(config.blacklisted, [])
config.denylisted = None
self.assertEqual(config.denylisted, [])

def test_blacklisted_setter_empty(self):
def test_denylisted_setter_empty(self):
config = Configuration()
config.blacklisted = ''
self.assertEqual(config.blacklisted, [])
config.denylisted = ''
self.assertEqual(config.denylisted, [])

def test_request_timeout_setter(self):
config = Configuration()
Expand Down
40 changes: 26 additions & 14 deletions castle/test/extractors/headers_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from castle.test import unittest
from castle.configuration import configuration
from castle.configuration import configuration, DEFAULT_ALLOWLIST
from castle.extractors.headers import ExtractorsHeaders


Expand All @@ -18,8 +18,8 @@ def formatted_headers():
class ExtractorsHeadersTestCase(unittest.TestCase):

def tearDown(self):
configuration.whitelisted = []
configuration.blacklisted = []
configuration.allowlisted = []
configuration.denylisted = []

def test_extract_headers(self):
self.assertEqual(ExtractorsHeaders(formatted_headers()).call(),
Expand All @@ -32,8 +32,8 @@ def test_extract_headers(self):
'X-Forwarded-For': '1.2.3.4'
})

def test_whitelisted_headers(self):
configuration.whitelisted = ['Accept', 'OK']
def test_allowlisted_headers(self):
configuration.allowlisted = ['Accept', 'OK']
self.assertEqual(
ExtractorsHeaders(formatted_headers()).call(),
{'Accept': 'application/json',
Expand All @@ -45,10 +45,23 @@ def test_whitelisted_headers(self):
'X-Forwarded-For': True
}
)
#

def test_restricted_blacklisted_headers(self):
configuration.blacklisted = ['User-Agent']
def test_only_default_allowlisted_headers(self):
configuration.allowlisted = DEFAULT_ALLOWLIST
self.assertEqual(
ExtractorsHeaders(formatted_headers()).call(),
{'Accept': 'application/json',
'Authorization': True,
'Cookie': True,
'Ok': True,
'Content-Length': '0',
'User-Agent': 'Mozilla 1234',
'X-Forwarded-For': True
}
)

def test_restricted_denylisted_headers(self):
configuration.denylisted = ['User-Agent']
self.assertEqual(
ExtractorsHeaders(formatted_headers()).call(),
{'Accept': 'application/json',
Expand All @@ -61,8 +74,8 @@ def test_restricted_blacklisted_headers(self):
}
)

def test_blacklisted_headers(self):
configuration.blacklisted = ['Accept']
def test_denylisted_headers(self):
configuration.denylisted = ['Accept']
self.assertEqual(
ExtractorsHeaders(formatted_headers()).call(),
{'Accept': True,
Expand All @@ -74,11 +87,10 @@ def test_blacklisted_headers(self):
'X-Forwarded-For': '1.2.3.4'
}
)
#

def test_blacklisted_and_whitelisted_headers(self):
configuration.blacklisted = ['Accept']
configuration.whitelisted = ['Accept']
def test_denylisted_and_allowlisted_headers(self):
configuration.denylisted = ['Accept']
configuration.allowlisted = ['Accept']
self.assertEqual(
ExtractorsHeaders(formatted_headers()).call()['Accept'], True
)