Skip to content
Merged

Dev #43

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
29 changes: 25 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ You can also create request context from requests:

```python
from securenative.securenative import SecureNative
from securenative.context.securenative_context import SecureNativeContext
from securenative.models.event_options import EventOptions
from securenative.enums.event_types import EventTypes
from securenative.models.user_traits import UserTraits
Expand All @@ -120,7 +119,7 @@ from securenative.models.user_traits import UserTraits
def track(request):
securenative = SecureNative.get_instance()

context = SecureNativeContext.from_http_request(request)
context = securenative.from_http_request(request)
event_options = EventOptions(event=EventTypes.LOG_IN,
user_id="1234",
user_traits=UserTraits("Your Name", "name@gmail.com", "+1234567890"),
Expand All @@ -137,15 +136,14 @@ def track(request):
```python
from securenative.securenative import SecureNative
from securenative.models.event_options import EventOptions
from securenative.context.securenative_context import SecureNativeContext
from securenative.enums.event_types import EventTypes
from securenative.models.user_traits import UserTraits


def verify(request):
securenative = SecureNative.get_instance()

context = SecureNativeContext.from_http_request(request)
context = securenative.from_http_request(request)
event_options = EventOptions(event=EventTypes.LOG_IN,
user_id="1234",
user_traits=UserTraits("Your Name", "name@gmail.com", "+1234567890"),
Expand Down Expand Up @@ -173,3 +171,26 @@ def webhook_endpoint(request):
is_verified = securenative.verify_request_payload(request)
```

## Extract proxy headers from Cloudflare

You can specify custom header keys to allow extraction of client ip from different providers.
This example demonstrates the usage of proxy headers for ip extraction from Cloudflare.

### Option 1: Using config file
```ini
SECURENATIVE_API_KEY: dsbe27fh3437r2yd326fg3fdg36f43
SECURENATIVE_PROXY_HEADERS: ["CF-Connecting-IP"]
```

Initialize sdk as shown above.

### Options 2: Using ConfigurationBuilder

```python
from securenative.securenative import SecureNative
from securenative.config.securenative_options import SecureNativeOptions


options = SecureNativeOptions(api_key="YOUR_API_KEY", max_events=10, log_level="ERROR", proxy_headers=['CF-Connecting-IP'])
securenative = SecureNative.init_with_options(options)
```
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.1
0.3.2
6 changes: 3 additions & 3 deletions securenative/api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ def verify(self, event_options):
Logger.debug("Verify event call")
event = SDKEvent(event_options, self.options)
try:
res = json.loads(self.event_manager.send_sync(event, ApiRoute.VERIFY.value, False).text)
res = json.loads(self.event_manager.send_sync(event, ApiRoute.VERIFY.value).text)
return VerifyResult(res["riskLevel"], res["score"], res["triggers"])
except Exception as e:
Logger.debug("Failed to call verify; {}".format(e))
if self.options.fail_over_strategy is FailOverStrategy.FAIL_OPEN.value:
return VerifyResult(RiskLevel.LOW.value, 0, None)
return VerifyResult(RiskLevel.HIGH.value, 1, None)
return VerifyResult(RiskLevel.LOW.value, 0, [])
return VerifyResult(RiskLevel.HIGH.value, 1, [])
8 changes: 5 additions & 3 deletions securenative/config/configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from configparser import ConfigParser

from securenative.config.securenative_options import SecureNativeOptions
from securenative.exceptions.securenative_config_exception import SecureNativeConfigException
from securenative.logger import Logger


class ConfigurationManager(object):
Expand All @@ -15,7 +15,7 @@ def read_resource_file(cls, resource_path):
try:
cls.config.read(resource_path)
except Exception as e:
raise SecureNativeConfigException("Invalid config file; %s", e)
Logger.debug("Invalid config file; {}, using default options".format(e))

properties = {}
for key, value in cls.config.defaults().items():
Expand Down Expand Up @@ -63,4 +63,6 @@ def load_config(cls, resource_path):
options.log_level),
fail_over_strategy=cls._get_env_or_default(properties,
"SECURENATIVE_FAILOVER_STRATEGY",
options.fail_over_strategy))
options.fail_over_strategy),
proxy_headers=cls._get_env_or_default(properties, "SECURENATIVE_PROXY_HEADERS",
options.proxy_headers))
5 changes: 4 additions & 1 deletion securenative/config/securenative_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ class SecureNativeOptions(object):

def __init__(self, api_key=None, api_url="https://api.securenative.com/collector/api/v1", interval=1000,
max_events=1000, timeout=1500, auto_send=True, disable=False, log_level="CRITICAL",
fail_over_strategy=FailOverStrategy.FAIL_OPEN.value):
fail_over_strategy=FailOverStrategy.FAIL_OPEN.value, proxy_headers=None):

if proxy_headers is None:
proxy_headers = []
if fail_over_strategy != FailOverStrategy.FAIL_OPEN.value and \
fail_over_strategy != FailOverStrategy.FAIL_CLOSED.value:
self.fail_over_strategy = FailOverStrategy.FAIL_OPEN.value
Expand All @@ -21,3 +23,4 @@ def __init__(self, api_key=None, api_url="https://api.securenative.com/collector
self.auto_send = auto_send
self.disable = disable
self.log_level = log_level
self.proxy_headers = proxy_headers
4 changes: 2 additions & 2 deletions securenative/context/securenative_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self, client_token=None, ip=None, remote_ip=None, headers=None, url
self.body = body

@staticmethod
def from_http_request(request):
def from_http_request(request, options):
try:
client_token = request.cookies[RequestUtils.SECURENATIVE_COOKIE]
except Exception:
Expand All @@ -28,6 +28,6 @@ def from_http_request(request):
if Utils.is_null_or_empty(client_token):
client_token = RequestUtils.get_secure_header_from_request(headers)

return SecureNativeContext(client_token, RequestUtils.get_client_ip_from_request(request),
return SecureNativeContext(client_token, RequestUtils.get_client_ip_from_request(request, options),
RequestUtils.get_remote_ip_from_request(request), headers, request.url,
request.method, None)
16 changes: 4 additions & 12 deletions securenative/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def flush(self):
for item in self.queue:
self.http_client.post(item.url, item.body)

def send_sync(self, event, resource_path, retry):
def send_sync(self, event, resource_path):
if self.options.disable:
Logger.warning("SDK is disabled. no operation will be performed")
return
Expand All @@ -64,17 +64,9 @@ def send_sync(self, event, resource_path, retry):
resource_path,
json.dumps(EventManager.serialize(event))
)
if res.status_code != 200:
Logger.info("SecureNative failed to call endpoint {} with event {}. adding back to queue".format(
resource_path, event))
item = QueueItem(
resource_path,
json.dumps(EventManager.serialize(event)),
retry
)
self.queue.append(item)
if self._is_queue_full():
self.queue = self.queue[:len(self.queue - 1)]
if res is None or res.status_code != 200:
Logger.info("SecureNative failed to call endpoint {} with event {}.".format(resource_path, event))

return res

def _is_queue_full(self):
Expand Down
7 changes: 6 additions & 1 deletion securenative/http/securenative_http_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import requests
from requests import Timeout

from securenative.utils.version_utils import VersionUtils

Expand All @@ -24,4 +25,8 @@ def _headers(self):

def post(self, path, body):
url = "{}/{}".format(self.options.api_url, path)
return requests.post(url=url, data=body, headers=self._headers())
try:
res = requests.post(url=url, data=body, headers=self._headers(), timeout=self.options.timeout / 1000)
return res
except Timeout:
return None
4 changes: 4 additions & 0 deletions securenative/securenative.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from securenative.api_manager import ApiManager
from securenative.config.configuration_manager import ConfigurationManager
from securenative.config.securenative_options import SecureNativeOptions
from securenative.context.securenative_context import SecureNativeContext
from securenative.event_manager import EventManager
from securenative.exceptions.securenative_config_exception import SecureNativeConfigException
from securenative.exceptions.securenative_sdk_Illegal_state_exception import SecureNativeSDKIllegalStateException
Expand Down Expand Up @@ -75,6 +76,9 @@ def verify(self, event_options):
def _flush(cls):
cls._securenative = None

def from_http_request(self, request):
return SecureNativeContext.from_http_request(request, self._options)

def verify_request_payload(self, request):
request_signature = request.header[SignatureUtils.SignatureHeader]
body = request.body
Expand Down
22 changes: 20 additions & 2 deletions securenative/utils/request_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,34 @@ def get_secure_header_from_request(headers):
return ""

@staticmethod
def get_client_ip_from_request(request):
def get_client_ip_from_request(request, options):
if options and len(options.proxy_headers) > 0:
for header in options.proxy_headers:
try:
if request.environ.get(header) is not None:
return request.environ.get(header)
if request.headers[header] is not None:
return request.headers[header]
except Exception:
try:
if request.headers[header] is not None:
return request.headers[header]
except Exception:
continue

try:
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[-1].strip()
else:
ip = request.META.get('REMOTE_ADDR')

if ip is None or ip == "":
ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', ""))

return ip
except Exception:
return request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', ""))
return ""

@staticmethod
def get_remote_ip_from_request(request):
Expand Down
2 changes: 1 addition & 1 deletion securenative/utils/version_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ class VersionUtils(object):

@staticmethod
def get_version():
return "0.3.1"
return "0.3.2"
20 changes: 19 additions & 1 deletion tests/api_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from securenative.enums.event_types import EventTypes
from securenative.enums.risk_level import RiskLevel
from securenative.event_manager import EventManager
from securenative.exceptions.securenative_invalid_options_exception import SecureNativeInvalidOptionsException
from securenative.models.event_options import EventOptions
from securenative.models.user_traits import UserTraits
from securenative.models.verify_result import VerifyResult
Expand Down Expand Up @@ -49,6 +48,25 @@ def test_track_event(self):
finally:
event_manager.stop_event_persist()

@responses.activate
def test_should_timeout_on_post(self):
options = SecureNativeOptions(api_key="YOUR_API_KEY", auto_send=True, timeout=0,
api_url="https://api.securenative-stg.com/collector/api/v1")

responses.add(responses.POST, "https://api.securenative-stg.com/collector/api/v1/verify",
json={"event": "SOME_EVENT_NAME"}, status=408)

event_manager = EventManager(options)
event_manager.start_event_persist()
api_manager = ApiManager(event_manager, options)

verify_result = VerifyResult(RiskLevel.LOW.value, 0, [])
res = api_manager.verify(self.event_options)

self.assertEqual(res.risk_level, verify_result.risk_level)
self.assertEqual(res.score, verify_result.score)
self.assertEqual(res.triggers, verify_result.triggers)

@responses.activate
def test_verify_event(self):
options = SecureNativeOptions(api_key="YOUR_API_KEY",
Expand Down
4 changes: 2 additions & 2 deletions tests/context_builder_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_create_context_from_request(self):
request.headers = {
"x-securenative": "71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a"}

context = SecureNativeContext.from_http_request(request)
context = SecureNativeContext.from_http_request(request, None)

self.assertEqual(context.client_token,
"71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a")
Expand All @@ -47,7 +47,7 @@ def test_create_context_from_request_with_cookie(self):
request.cookies = {"_sn":
"71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a"}

context = SecureNativeContext.from_http_request(request)
context = SecureNativeContext.from_http_request(request, None)

self.assertEqual(context.client_token,
"71532c1fad2c7f56118f7969e401f3cf080239140d208e7934e6a530818c37e544a0c2330a487bcc6fe4f662a57f265a3ed9f37871e80529128a5e4f2ca02db0fb975ded401398f698f19bb0cafd68a239c6caff99f6f105286ab695eaf3477365bdef524f5d70d9be1d1d474506b433aed05d7ed9a435eeca357de57817b37c638b6bb417ffb101eaf856987615a77a")
Expand Down
9 changes: 0 additions & 9 deletions tests/encryption_utils_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import unittest

import pytest

from securenative.utils.encryption_utils import EncryptionUtils


Expand All @@ -19,10 +17,3 @@ def test_decrypt(self):

self.assertEqual(result.cid, self.CID)
self.assertEqual(result.fp, self.FP)

@pytest.mark.skip("Differences in crypto version fails this test when in reality it's passing")
def test_encrypt(self):
result = EncryptionUtils.encrypt(self.PAYLOAD, self.SECRET_KEY)

self.assertIsNotNone(result)
self.assertGreater(len(self.PAYLOAD), len(result))
6 changes: 3 additions & 3 deletions tests/event_manager_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_should_successfully_send_sync_event_with_status_code_200(self):
json=json.loads(res_body), status=200)
event_manager = EventManager(options)

data = event_manager.send_sync(self.event, "some-path/to-api", False)
data = event_manager.send_sync(self.event, "some-path/to-api")
self.assertEqual(res_body, data.text)

@responses.activate
Expand All @@ -49,7 +49,7 @@ def test_should_send_sync_event_and_fail_when_status_code_401(self):
json={}, status=401)
event_manager = EventManager(options)

res = event_manager.send_sync(self.event, "some-path/to-api", False)
res = event_manager.send_sync(self.event, "some-path/to-api")

self.assertEqual(res.status_code, 401)

Expand All @@ -62,6 +62,6 @@ def test_should_send_sync_event_and_fail_when_status_code_500(self):
json={}, status=500)
event_manager = EventManager(options)

res = event_manager.send_sync(self.event, "some-path/to-api", False)
res = event_manager.send_sync(self.event, "some-path/to-api")

self.assertEqual(res.status_code, 500)
20 changes: 20 additions & 0 deletions tests/request_utils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest

import requests_mock

from securenative.config.securenative_options import SecureNativeOptions
from securenative.utils.request_utils import RequestUtils


class RequestUtilsTest(unittest.TestCase):

def test_proxy_headers_extraction_from_request(self):
options = SecureNativeOptions(proxy_headers=['CF-Connecting-IP'])

with requests_mock.Mocker(real_http=True) as request:
request.headers = {
"CF-Connecting-IP": "203.0.113.1"}

client_ip = RequestUtils.get_client_ip_from_request(request, options)

self.assertEqual(client_ip, "203.0.113.1")