From 37869d55afd5190d6f65218c2e8485a5e4892c8b Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 10:20:27 +0100 Subject: [PATCH 01/12] Remove events.py --- README.rst | 10 ++++------ castle/events.py | 42 ------------------------------------------ 2 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 castle/events.py diff --git a/README.rst b/README.rst index 9fe681a..a39b938 100644 --- a/README.rst +++ b/README.rst @@ -118,11 +118,10 @@ Here is a simple example of track event. .. code:: python from castle.client import Client - from castle import events castle = Client.from_request(request) castle.track({ - 'event': events.LOGIN_SUCCEEDED, + 'event': '$login', 'user_id': 'user_id' }) @@ -148,11 +147,10 @@ background worker you can generate data for a worker: .. code:: python from castle.payload.prepare import PayloadPrepare - from castle import events payload = PayloadPrepare.call( { - 'event': events.LOGIN_SUCCEEDED, + 'event': '$login', 'user_id': user.id, 'properties': { 'key': 'value' }, 'user_traits': { 'key': 'value' } @@ -172,7 +170,7 @@ and use it later in a way Events -------------- -List of Recognized Events can be found `here `_ or in the `docs `_. +List of Recognized Events can be found in the `docs `_. Device management ----------------- @@ -234,7 +232,7 @@ error `__. Webhooks -------- -Castle uses webhooks to notify about ``$incident.confirmed`` or `$review.opened` events. +Castle uses webhooks to notify about ``$incident.confirmed`` or ``$review.opened`` events. Each webhook has ``X-Castle-Signature`` header that allows verifying webhook's source. .. code:: python diff --git a/castle/events.py b/castle/events.py deleted file mode 100644 index d9310af..0000000 --- a/castle/events.py +++ /dev/null @@ -1,42 +0,0 @@ -# Record when a user succesfully logs in. -LOGIN_SUCCEEDED = '$login.succeeded' -# Record when a user failed to log in. -LOGIN_FAILED = '$login.failed' -# Record when a user logs out. -LOGOUT_SUCCEEDED = '$logout.succeeded' -# Record when a user updated their profile (including password, email, phone, etc). -PROFILE_UPDATE_SUCCEEDED = '$profile_update.succeeded' -# Record errors when updating profile. -PROFILE_UPDATE_FAILED = '$profile_update.failed' -# Capture account creation, both when a user signs up as well as when created manually -# by an administrator. -REGISTRATION_SUCCEEDED = '$registration.succeeded' -# Record when an account failed to be created. -REGISTRATION_FAILED = '$registration.failed' -# The user completed all of the steps in the password reset process and the password was -# successfully reset.Password resets do not required knowledge of the current password. -PASSWORD_RESET_SUCCEEDED = '$password_reset.succeeded' -# Use to record when a user failed to reset their password. -PASSWORD_RESET_FAILED = '$password_reset.failed' -# The user successfully requested a password reset. -PASSWORD_RESET_REQUEST_SUCCCEEDED = '$password_reset_request.succeeded' -# The user failed to request a password reset. -PASSWORD_RESET_REQUEST_FAILED = '$password_reset_request.failed' -# User account has been reset. -INCIDENT_MITIGATED = '$incident.mitigated' -# User confirmed malicious activity. -REVIEW_ESCALATED = '$review.escalated' -# User confirmed safe activity. -REVIEW_RESOLVED = '$review.resolved' -# Record when a user is prompted with additional verification, such as two-factor -# authentication or a captcha. -CHALLENGE_REQUESTED = '$challenge.requested' -# Record when additional verification was successful. -CHALLENGE_SUCCEEDED = '$challenge.succeeded' -# Record when additional verification failed. -CHALLENGE_FAILED = '$challenge.failed' -# Record when a user attempts an in-app transaction, such as a purchase or withdrawal. -TRANSACTION_ATTEMPTED = '$transaction.attempted' -# Record when a user session is extended, or use any time you want -# to re-authenticate a user mid-session. -SESSION_EXTENDED = '$session.extended' From 38c6ad4d4562cba43abb56a2723ce956845c9065 Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 10:24:34 +0100 Subject: [PATCH 02/12] Remove identify and review --- castle/api/review.py | 9 ---- castle/client.py | 6 --- castle/commands/identify.py | 20 ------- castle/commands/review.py | 15 ------ castle/test/__init__.py | 3 -- castle/test/api/review_test.py | 27 ---------- castle/test/client_test.py | 18 ------- castle/test/commands/identify_test.py | 76 --------------------------- castle/test/commands/review_test.py | 21 -------- 9 files changed, 195 deletions(-) delete mode 100644 castle/api/review.py delete mode 100644 castle/commands/identify.py delete mode 100644 castle/commands/review.py delete mode 100644 castle/test/api/review_test.py delete mode 100644 castle/test/commands/identify_test.py delete mode 100644 castle/test/commands/review_test.py diff --git a/castle/api/review.py b/castle/api/review.py deleted file mode 100644 index d9c3b27..0000000 --- a/castle/api/review.py +++ /dev/null @@ -1,9 +0,0 @@ -from castle.api_request import APIRequest -from castle.commands.review import CommandsReview -from castle.configuration import configuration - - -class APIReview(object): - @staticmethod - def call(review_id, config=configuration): - return APIRequest(config).call(CommandsReview.call(review_id)) diff --git a/castle/client.py b/castle/client.py index 7e0e9d2..c68923a 100644 --- a/castle/client.py +++ b/castle/client.py @@ -1,6 +1,5 @@ from castle.api_request import APIRequest from castle.commands.authenticate import CommandsAuthenticate -from castle.commands.identify import CommandsIdentify from castle.commands.start_impersonation import CommandsStartImpersonation from castle.commands.end_impersonation import CommandsEndImpersonation from castle.commands.track import CommandsTrack @@ -58,11 +57,6 @@ def authenticate(self, options): 'Castle set to do not track.' ).call() - def identify(self, options): - if not self.tracked(): - return None - self._add_timestamp_if_necessary(options) - return self.api.call(CommandsIdentify(self.context).call(options)) def start_impersonation(self, options): self._add_timestamp_if_necessary(options) diff --git a/castle/commands/identify.py b/castle/commands/identify.py deleted file mode 100644 index 43ba523..0000000 --- a/castle/commands/identify.py +++ /dev/null @@ -1,20 +0,0 @@ -from castle.command import Command -from castle.utils.timestamp import UtilsTimestamp as generate_timestamp -from castle.context.merge import ContextMerge -from castle.context.sanitize import ContextSanitize -from castle.validators.not_supported import ValidatorsNotSupported - - -class CommandsIdentify(object): - def __init__(self, context): - self.context = context - - def call(self, options): - ValidatorsNotSupported.call(options, 'properties') - context = ContextMerge.call(self.context, options.get('context')) - context = ContextSanitize.call(context) - if context: - options.update({'context': context}) - options.update({'sent_at': generate_timestamp.call()}) - - return Command(method='post', path='identify', data=options) diff --git a/castle/commands/review.py b/castle/commands/review.py deleted file mode 100644 index d90c7b0..0000000 --- a/castle/commands/review.py +++ /dev/null @@ -1,15 +0,0 @@ -from castle.command import Command -from castle.validators.present import ValidatorsPresent - - -class CommandsReview(object): - - @staticmethod - def call(review_id): - ValidatorsPresent.call({'review_id': review_id}, 'review_id') - - return Command( - method='get', - path="reviews/{review_id}".format(review_id=review_id), - data=None - ) diff --git a/castle/test/__init__.py b/castle/test/__init__.py index 513f6e6..df17350 100644 --- a/castle/test/__init__.py +++ b/castle/test/__init__.py @@ -9,7 +9,6 @@ 'castle.test.api.get_device_test', 'castle.test.api.get_devices_for_user_test', 'castle.test.api.report_device_test', - 'castle.test.api.review_test', 'castle.test.api_request_test', 'castle.test.client_id.extract_test', 'castle.test.client_test', @@ -19,9 +18,7 @@ 'castle.test.commands.end_impersonation_test', 'castle.test.commands.get_device_test', 'castle.test.commands.get_devices_for_user_test', - 'castle.test.commands.identify_test', 'castle.test.commands.report_device_test', - 'castle.test.commands.review_test', 'castle.test.commands.start_impersonation_test', 'castle.test.commands.track_test', 'castle.test.configuration_test', diff --git a/castle/test/api/review_test.py b/castle/test/api/review_test.py deleted file mode 100644 index 29a27a6..0000000 --- a/castle/test/api/review_test.py +++ /dev/null @@ -1,27 +0,0 @@ -import json -import responses - -from castle.test import unittest -from castle.api.review import APIReview -from castle.configuration import configuration - - -class APIReviewTestCase(unittest.TestCase): - def setUp(self): - configuration.api_secret = 'test' - - def tearDown(self): - configuration.api_secret = None - - @responses.activate - def test_call(self): - # pylint: disable=line-too-long - response_text = "{\"id\":\"56b32fa0-880b-0135-74d6-00e650213316\",\"reviewed\":false,\"created_at\":\"2017-09-15T11:59:57.211Z\",\"user_id\":\"1\",\"context\":{\"ip\":\"8.8.8.8\",\"location\":{\"country_code\":\"US\",\"country\":\"United States\",\"region\":null,\"region_code\":null,\"city\":null,\"lat\":37.751,\"lon\":-97.822},\"user_agent\":{\"raw\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36\",\"browser\":\"Chrome\",\"version\":\"60.0.3112\",\"os\":\"Mac OS X 10.12.6\",\"mobile\":false,\"platform\":\"Mac OS X\",\"device\":\"Unknown\",\"family\":\"Chrome\"}}}" - responses.add( - responses.GET, - 'https://api.castle.io/v1/reviews/1234', - body=response_text, - status=200 - ) - review_id = '1234' - self.assertEqual(APIReview.call(review_id), json.loads(response_text)) diff --git a/castle/test/client_test.py b/castle/test/client_test.py index f3c5d19..9dd2907 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -99,24 +99,6 @@ def test_end_impersonation_failed(self): with self.assertRaises(ImpersonationFailed): client.end_impersonation(options) - @responses.activate - def test_identify_tracked_true(self): - response_text = 'identify' - responses.add( - responses.POST, - 'https://api.castle.io/v1/identify', - json=response_text, - status=200 - ) - client = Client.from_request(request(), {}) - options = {'event': '$login.authenticate', 'user_id': '1234'} - self.assertEqual(client.identify(options), response_text) - - def test_identify_tracked_false(self): - client = Client.from_request(request(), {}) - client.disable_tracking() - self.assertEqual(client.identify({}), None) - @responses.activate def test_authenticate_tracked_true(self): response_text = {'action': Verdict.ALLOW.value, 'user_id': '1234'} diff --git a/castle/test/commands/identify_test.py b/castle/test/commands/identify_test.py deleted file mode 100644 index 5c29489..0000000 --- a/castle/test/commands/identify_test.py +++ /dev/null @@ -1,76 +0,0 @@ -from castle.test import mock, unittest -from castle.command import Command -from castle.commands.identify import CommandsIdentify -from castle.errors import InvalidParametersError -from castle.utils.clone import UtilsClone - - -def default_options(): - """Default options include all required fields.""" - return {'user_id': '1234'} - - -def default_options_plus(**extra): - """Default options plus the given extra fields.""" - options = default_options() - options.update(extra) - return options - - -def default_command_with_data(**data): - """What we expect the identify command to look like.""" - return Command( - method='post', - path='identify', - data=dict(sent_at=mock.sentinel.timestamp, **data) - ) - - -class CommandsIdentifyTestCase(unittest.TestCase): - def setUp(self): - # patch timestamp to return a known value - timestamp_patcher = mock.patch('castle.commands.identify.generate_timestamp.call') - self.mock_timestamp = timestamp_patcher.start() - self.mock_timestamp.return_value = mock.sentinel.timestamp - self.addCleanup(timestamp_patcher.stop) - - def test_init(self): - context = mock.sentinel.test_init_context - obj = CommandsIdentify(context) - self.assertEqual(obj.context, context) - - def test_call(self): - context = {'test': '1'} - options = default_options_plus(context={'color': 'blue'}) - - # expect the original context to have been merged with the context specified in the options - expected_data = UtilsClone.call(options) - expected_data.update(context={'test': '1', 'color': 'blue'}) - expected = default_command_with_data(**expected_data) - - self.assertEqual(CommandsIdentify(context).call(options), expected) - - def test_call_no_user_id(self): - context = {} - options = default_options() - options.pop('user_id') - - expected = default_command_with_data(**options) - - self.assertEqual(CommandsIdentify(context).call(options), expected) - - def test_call_properties_not_allowed(self): - context = {'test': '1'} - options = default_options_plus(properties={'hair': 'blonde'}) - - with self.assertRaises(InvalidParametersError): - CommandsIdentify(context).call(options) - - def test_call_user_traits_allowed(self): - context = {} - options = default_options_plus(user_traits={'email': 'identity@its.me.com'}) - options.update({'context': context}) - - expected = default_command_with_data(**options) - - self.assertEqual(CommandsIdentify(context).call(options), expected) diff --git a/castle/test/commands/review_test.py b/castle/test/commands/review_test.py deleted file mode 100644 index bb4b5b2..0000000 --- a/castle/test/commands/review_test.py +++ /dev/null @@ -1,21 +0,0 @@ -from castle.test import unittest -from castle.command import Command -from castle.commands.review import CommandsReview -from castle.errors import InvalidParametersError - - -def review_id(): - return '1234' - - -class CommandsReviewTestCase(unittest.TestCase): - def test_call_no_review_id(self): - with self.assertRaises(InvalidParametersError): - CommandsReview.call('') - - def test_call(self): - command = CommandsReview.call(review_id()) - self.assertIsInstance(command, Command) - self.assertEqual(command.method, 'get') - self.assertEqual(command.path, "reviews/1234") - self.assertEqual(command.data, None) From 9194a36c049c61567295327a643a0c9390dd3108 Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 15:02:09 +0100 Subject: [PATCH 03/12] Rename client_id to fingerprint --- castle/client.py | 1 - castle/context/get_default.py | 8 +-- castle/{client_id => fingerprint}/__init__.py | 0 castle/{client_id => fingerprint}/extract.py | 2 +- castle/options/__init__.py | 0 castle/options/get_default.py | 53 +++++++++++++++++++ castle/test/__init__.py | 2 +- castle/test/client_id/extract_test.py | 43 --------------- castle/test/client_test.py | 2 +- castle/test/context/get_default_test.py | 8 +-- castle/test/context/prepare_test.py | 6 +-- castle/test/fingerprint/extract_test.py | 43 +++++++++++++++ castle/test/payload/prepare_test.py | 2 +- 13 files changed, 111 insertions(+), 59 deletions(-) rename castle/{client_id => fingerprint}/__init__.py (100%) rename castle/{client_id => fingerprint}/extract.py (86%) create mode 100644 castle/options/__init__.py create mode 100644 castle/options/get_default.py delete mode 100644 castle/test/client_id/extract_test.py create mode 100644 castle/test/fingerprint/extract_test.py diff --git a/castle/client.py b/castle/client.py index c68923a..98c5a05 100644 --- a/castle/client.py +++ b/castle/client.py @@ -57,7 +57,6 @@ def authenticate(self, options): 'Castle set to do not track.' ).call() - def start_impersonation(self, options): self._add_timestamp_if_necessary(options) response = self.api.call(CommandsStartImpersonation(self.context).call(options)) diff --git a/castle/context/get_default.py b/castle/context/get_default.py index ed20f97..8680039 100644 --- a/castle/context/get_default.py +++ b/castle/context/get_default.py @@ -1,6 +1,6 @@ from castle.version import VERSION from castle.headers.filter import HeadersFilter -from castle.client_id.extract import ClientIdExtract +from castle.fingerprint.extract import FingerprintExtract from castle.headers.extract import HeadersExtract from castle.ips.extract import IPsExtract @@ -14,7 +14,7 @@ def __init__(self, request, cookies): def call(self): context = dict({ - 'client_id': self._client_id(), + 'fingerprint': self._fingerprint(), 'active': True, 'headers': self._headers(), 'ip': self._ip(), @@ -30,8 +30,8 @@ def call(self): def _ip(self): return IPsExtract(self.pre_headers).call() - def _client_id(self): - return ClientIdExtract(self.pre_headers, self.cookies).call() + def _fingerprint(self): + return FingerprintExtract(self.pre_headers, self.cookies).call() def _headers(self): return HeadersExtract(self.pre_headers).call() diff --git a/castle/client_id/__init__.py b/castle/fingerprint/__init__.py similarity index 100% rename from castle/client_id/__init__.py rename to castle/fingerprint/__init__.py diff --git a/castle/client_id/extract.py b/castle/fingerprint/extract.py similarity index 86% rename from castle/client_id/extract.py rename to castle/fingerprint/extract.py index d555552..aa25250 100644 --- a/castle/client_id/extract.py +++ b/castle/fingerprint/extract.py @@ -1,4 +1,4 @@ -class ClientIdExtract(object): +class FingerprintExtract(object): def __init__(self, headers, cookies=None): self.headers = headers self.cookies = cookies or dict() diff --git a/castle/options/__init__.py b/castle/options/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/castle/options/get_default.py b/castle/options/get_default.py new file mode 100644 index 0000000..6e019ed --- /dev/null +++ b/castle/options/get_default.py @@ -0,0 +1,53 @@ +from castle.version import VERSION +from castle.headers.filter import HeadersFilter +from castle.fingerprint.extract import FingerprintExtract +from castle.headers.extract import HeadersExtract +from castle.ips.extract import IPsExtract + +__version__ = VERSION + + +class OptionsGetDefault(object): + def __init__(self, request, cookies): + self.cookies = self._fetch_cookies(request, cookies) + self.pre_headers = HeadersFilter(request).call() + + def call(self): + context = dict({ + 'fingerprint': self._fingerprint(), + 'active': True, + 'headers': self._headers(), + 'ip': self._ip(), + 'library': { + 'name': 'castle-python', + 'version': __version__ + } + }) + context.update(self._optional_defaults()) + + return context + + def _ip(self): + return IPsExtract(self.pre_headers).call() + + def _fingerprint(self): + return FingerprintExtract(self.pre_headers, self.cookies).call() + + def _headers(self): + return HeadersExtract(self.pre_headers).call() + + def _optional_defaults(self): + context = dict() + if 'Accept-Language' in self.pre_headers: + context['locale'] = self.pre_headers.get('Accept-Language') + if 'User-Agent' in self.pre_headers: + context['user_agent'] = self.pre_headers.get('User-Agent') + return context + + @staticmethod + def _fetch_cookies(request, cookies): + if cookies: + return cookies + if hasattr(request, 'COOKIES') and request.COOKIES: + return request.COOKIES + return None diff --git a/castle/test/__init__.py b/castle/test/__init__.py index df17350..bd05158 100644 --- a/castle/test/__init__.py +++ b/castle/test/__init__.py @@ -10,7 +10,7 @@ 'castle.test.api.get_devices_for_user_test', 'castle.test.api.report_device_test', 'castle.test.api_request_test', - 'castle.test.client_id.extract_test', + 'castle.test.fingerprint.extract_test', 'castle.test.client_test', 'castle.test.command_test', 'castle.test.commands.approve_device_test', diff --git a/castle/test/client_id/extract_test.py b/castle/test/client_id/extract_test.py deleted file mode 100644 index c3585cb..0000000 --- a/castle/test/client_id/extract_test.py +++ /dev/null @@ -1,43 +0,0 @@ -from castle.test import unittest - -from castle.client_id.extract import ClientIdExtract - - -def client_id(): - return 'cookies' - - -def client_id_environ(): - return 'environ' - - -def cookies(): - return {'__cid': client_id()} - - -def environ(): - return {'X-Castle-Client-Id': client_id_environ()} - - -class ClientIdExtractTestCase(unittest.TestCase): - def test_extract_client_id_from_cookiesand_environ(self): - self.assertEqual( - ClientIdExtract(environ(), cookies()).call(), - client_id_environ() - ) - - def test_extract_client_id_from_cookies(self): - self.assertEqual( - ClientIdExtract({}, cookies()).call(), - client_id() - ) - - def test_extract_client_id_from_environ(self): - self.assertEqual(ClientIdExtract( - environ(), {}).call(), client_id_environ()) - - def test_extract_client_id_unavailable(self): - self.assertEqual(ClientIdExtract({}, {}).call(), '') - - def test_extract_client_id_no_cookies(self): - self.assertEqual(ClientIdExtract({}).call(), '') diff --git a/castle/test/client_test.py b/castle/test/client_test.py index 9dd2907..e227a79 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -30,7 +30,7 @@ def tearDown(self): def test_init(self): context = { 'active': True, - 'client_id': '1234', + 'fingerprint': '1234', 'headers': { 'User-Agent': 'test', 'X-Forwarded-For': '217.144.192.112', diff --git a/castle/test/context/get_default_test.py b/castle/test/context/get_default_test.py index fcb33f8..b1b3e64 100644 --- a/castle/test/context/get_default_test.py +++ b/castle/test/context/get_default_test.py @@ -4,12 +4,12 @@ from castle.context.get_default import ContextGetDefault -def client_id(): +def fingerprint(): return 'abcd' def cookies(): - return {'__cid': client_id()} + return {'__cid': fingerprint()} def request_ip(): @@ -19,7 +19,7 @@ def request_ip(): def environ(): return { 'HTTP_X_FORWARDED_FOR': request_ip(), - 'HTTP_COOKIE': "__cid={client_id()};other=efgh", + 'HTTP_COOKIE': "__cid={fingerprint()};other=efgh", 'HTTP-Accept-Language': 'en', 'HTTP-User-Agent': 'test' } @@ -37,7 +37,7 @@ class ContextGetDefaultTestCase(unittest.TestCase): def test_default_context(self): context = ContextGetDefault( request(environ()), cookies()).call() - self.assertEqual(context['client_id'], client_id()) + self.assertEqual(context['fingerprint'], fingerprint()) self.assertEqual(context['active'], True) self.assertEqual( context['headers'], diff --git a/castle/test/context/prepare_test.py b/castle/test/context/prepare_test.py index 0835e0a..6076d6d 100644 --- a/castle/test/context/prepare_test.py +++ b/castle/test/context/prepare_test.py @@ -19,7 +19,7 @@ class ContextPrepareTestCase(unittest.TestCase): def test_call(self): context = { 'active': True, - 'client_id': '1234', + 'fingerprint': '1234', 'headers': { 'User-Agent': 'test', 'X-Forwarded-For': '217.144.192.112', @@ -32,8 +32,8 @@ def test_call(self): result_context = ContextPrepare.call(request(), {}) self.assertEqual(result_context, context) - def test_setup_client_id_from_cookies(self): + def test_setup_fingerprint_from_cookies(self): cookies = {'__cid': '1234'} options = {'cookies': cookies} result_context = ContextPrepare.call(request(), options) - self.assertEqual(result_context['client_id'], '1234') + self.assertEqual(result_context['fingerprint'], '1234') diff --git a/castle/test/fingerprint/extract_test.py b/castle/test/fingerprint/extract_test.py new file mode 100644 index 0000000..67986cf --- /dev/null +++ b/castle/test/fingerprint/extract_test.py @@ -0,0 +1,43 @@ +from castle.test import unittest + +from castle.fingerprint.extract import FingerprintExtract + + +def fingerprint(): + return 'cookies' + + +def fingerprint_environ(): + return 'environ' + + +def cookies(): + return {'__cid': fingerprint()} + + +def environ(): + return {'X-Castle-Client-Id': fingerprint_environ()} + + +class FingerprintExtractTestCase(unittest.TestCase): + def test_extract_fingerprint_from_cookiesand_environ(self): + self.assertEqual( + FingerprintExtract(environ(), cookies()).call(), + fingerprint_environ() + ) + + def test_extract_fingerprint_from_cookies(self): + self.assertEqual( + FingerprintExtract({}, cookies()).call(), + fingerprint() + ) + + def test_extract_fingerprint_from_environ(self): + self.assertEqual(FingerprintExtract( + environ(), {}).call(), fingerprint_environ()) + + def test_extract_fingerprint_unavailable(self): + self.assertEqual(FingerprintExtract({}, {}).call(), '') + + def test_extract_fingerprint_no_cookies(self): + self.assertEqual(FingerprintExtract({}).call(), '') diff --git a/castle/test/payload/prepare_test.py b/castle/test/payload/prepare_test.py index 4b8ddfc..8b46dd5 100644 --- a/castle/test/payload/prepare_test.py +++ b/castle/test/payload/prepare_test.py @@ -17,7 +17,7 @@ def request(): def ctx(): return { 'active': True, - 'client_id': '1234', + 'fingerprint': '1234', 'headers': { 'User-Agent': 'test', 'X-Castle-Client-Id': '1234', From d55471c6448d4fa0767052c51ac8441a02f6f93f Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 15:09:32 +0100 Subject: [PATCH 04/12] Remove user agent --- castle/commands/end_impersonation.py | 2 +- castle/commands/start_impersonation.py | 2 +- castle/context/get_default.py | 9 --------- castle/options/get_default.py | 14 -------------- castle/test/client_test.py | 3 +-- castle/test/commands/end_impersonation_test.py | 14 +++----------- castle/test/commands/start_impersonation_test.py | 8 -------- castle/test/context/get_default_test.py | 1 - castle/test/context/prepare_test.py | 3 +-- castle/test/payload/prepare_test.py | 3 +-- 10 files changed, 8 insertions(+), 51 deletions(-) diff --git a/castle/commands/end_impersonation.py b/castle/commands/end_impersonation.py index 4bed078..8d631f3 100644 --- a/castle/commands/end_impersonation.py +++ b/castle/commands/end_impersonation.py @@ -14,7 +14,7 @@ def call(self, options): context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) - ValidatorsPresent.call(context, 'user_agent', 'ip') + ValidatorsPresent.call(context, 'ip') if context: options.update({'context': context}) diff --git a/castle/commands/start_impersonation.py b/castle/commands/start_impersonation.py index 83001e5..8ba3fab 100644 --- a/castle/commands/start_impersonation.py +++ b/castle/commands/start_impersonation.py @@ -14,7 +14,7 @@ def call(self, options): context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) - ValidatorsPresent.call(context, 'user_agent', 'ip') + ValidatorsPresent.call(context, 'ip') if context: options.update({'context': context}) diff --git a/castle/context/get_default.py b/castle/context/get_default.py index 8680039..7f866b1 100644 --- a/castle/context/get_default.py +++ b/castle/context/get_default.py @@ -23,7 +23,6 @@ def call(self): 'version': __version__ } }) - context.update(self._optional_defaults()) return context @@ -36,14 +35,6 @@ def _fingerprint(self): def _headers(self): return HeadersExtract(self.pre_headers).call() - def _optional_defaults(self): - context = dict() - if 'Accept-Language' in self.pre_headers: - context['locale'] = self.pre_headers.get('Accept-Language') - if 'User-Agent' in self.pre_headers: - context['user_agent'] = self.pre_headers.get('User-Agent') - return context - @staticmethod def _fetch_cookies(request, cookies): if cookies: diff --git a/castle/options/get_default.py b/castle/options/get_default.py index 6e019ed..58ab389 100644 --- a/castle/options/get_default.py +++ b/castle/options/get_default.py @@ -15,15 +15,9 @@ def __init__(self, request, cookies): def call(self): context = dict({ 'fingerprint': self._fingerprint(), - 'active': True, 'headers': self._headers(), 'ip': self._ip(), - 'library': { - 'name': 'castle-python', - 'version': __version__ - } }) - context.update(self._optional_defaults()) return context @@ -36,14 +30,6 @@ def _fingerprint(self): def _headers(self): return HeadersExtract(self.pre_headers).call() - def _optional_defaults(self): - context = dict() - if 'Accept-Language' in self.pre_headers: - context['locale'] = self.pre_headers.get('Accept-Language') - if 'User-Agent' in self.pre_headers: - context['user_agent'] = self.pre_headers.get('User-Agent') - return context - @staticmethod def _fetch_cookies(request, cookies): if cookies: diff --git a/castle/test/client_test.py b/castle/test/client_test.py index e227a79..ea82f0b 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -37,8 +37,7 @@ def test_init(self): 'X-Castle-Client-Id': '1234' }, 'ip': '217.144.192.112', - 'library': {'name': 'castle-python', 'version': VERSION}, - 'user_agent': 'test' + 'library': {'name': 'castle-python', 'version': VERSION} } client = Client.from_request(request(), {}) self.assertEqual(client.do_not_track, False) diff --git a/castle/test/commands/end_impersonation_test.py b/castle/test/commands/end_impersonation_test.py index 0594fb4..2ecdfc7 100644 --- a/castle/test/commands/end_impersonation_test.py +++ b/castle/test/commands/end_impersonation_test.py @@ -8,7 +8,7 @@ def default_options(): """Default options include all required fields.""" return {'properties': {'impersonator': 'admin'}, 'user_id': '1234', - 'context': {'ip': '127.0.0.1', 'user_agent': 'Chrome'}} + 'context': {'ip': '127.0.0.1'}} def default_options_plus(**extra): @@ -44,14 +44,14 @@ def test_init(self): def test_call(self): context = {'lang': 'es'} options = default_options_plus( - context={'local time': '8:53pm', 'ip': '127.0.0.1', 'user_agent': 'Chrome'} + context={'local time': '8:53pm', 'ip': '127.0.0.1'} ) # expect the original context to have been merged with the context specified in the options expected_data = UtilsClone.call(options) expected_data.update( context={'lang': 'es', 'local time': '8:53pm', - 'ip': '127.0.0.1', 'user_agent': 'Chrome'} + 'ip': '127.0.0.1'} ) expected = default_command_with_data(**expected_data) @@ -72,11 +72,3 @@ def test_call_no_context_ip(self): with self.assertRaises(InvalidParametersError): CommandsEndImpersonation(context).call(options) - - def test_call_no_context_user_agent(self): - context = {} - options = default_options() - options['context'].pop('user_agent') - - with self.assertRaises(InvalidParametersError): - CommandsEndImpersonation(context).call(options) diff --git a/castle/test/commands/start_impersonation_test.py b/castle/test/commands/start_impersonation_test.py index 9730756..701f325 100644 --- a/castle/test/commands/start_impersonation_test.py +++ b/castle/test/commands/start_impersonation_test.py @@ -72,11 +72,3 @@ def test_call_no_context_ip(self): with self.assertRaises(InvalidParametersError): CommandsStartImpersonation(context).call(options) - - def test_call_no_context_user_agent(self): - context = {} - options = default_options() - options['context'].pop('user_agent') - - with self.assertRaises(InvalidParametersError): - CommandsStartImpersonation(context).call(options) diff --git a/castle/test/context/get_default_test.py b/castle/test/context/get_default_test.py index b1b3e64..939d64b 100644 --- a/castle/test/context/get_default_test.py +++ b/castle/test/context/get_default_test.py @@ -53,4 +53,3 @@ def test_default_context(self): context['library'], {'name': 'castle-python', 'version': __version__} ) - self.assertEqual(context['user_agent'], 'test') diff --git a/castle/test/context/prepare_test.py b/castle/test/context/prepare_test.py index 6076d6d..b1502c7 100644 --- a/castle/test/context/prepare_test.py +++ b/castle/test/context/prepare_test.py @@ -26,8 +26,7 @@ def test_call(self): 'X-Castle-Client-Id': '1234' }, 'ip': '217.144.192.112', - 'library': {'name': 'castle-python', 'version': VERSION}, - 'user_agent': 'test' + 'library': {'name': 'castle-python', 'version': VERSION} } result_context = ContextPrepare.call(request(), {}) self.assertEqual(result_context, context) diff --git a/castle/test/payload/prepare_test.py b/castle/test/payload/prepare_test.py index 8b46dd5..0828132 100644 --- a/castle/test/payload/prepare_test.py +++ b/castle/test/payload/prepare_test.py @@ -24,8 +24,7 @@ def ctx(): 'X-Forwarded-For': '217.144.192.112' }, 'ip': '217.144.192.112', - 'library': {'name': 'castle-python', 'version': VERSION}, - 'user_agent': 'test' + 'library': {'name': 'castle-python', 'version': VERSION} } From 772074a9d02007e12df1c3b09efdf88b70620fb3 Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 16:13:49 +0100 Subject: [PATCH 05/12] Remove unused options from context --- castle/client.py | 4 +- castle/commands/end_impersonation.py | 1 - castle/commands/start_impersonation.py | 1 - castle/context/get_default.py | 30 +------------ castle/context/prepare.py | 5 +-- castle/payload/prepare.py | 4 +- castle/test/client_test.py | 7 ---- .../test/commands/end_impersonation_test.py | 8 ---- .../test/commands/start_impersonation_test.py | 8 ---- castle/test/context/get_default_test.py | 42 +------------------ castle/test/context/prepare_test.py | 15 +------ castle/test/payload/prepare_test.py | 7 ---- 12 files changed, 10 insertions(+), 122 deletions(-) diff --git a/castle/client.py b/castle/client.py index 98c5a05..64b4a52 100644 --- a/castle/client.py +++ b/castle/client.py @@ -13,11 +13,11 @@ class Client(object): @classmethod - def from_request(cls, request, options=None): + def from_request(cls, _request, options=None): if options is None: options = {} - options.setdefault('context', ContextPrepare.call(request, options)) + options.setdefault('context', ContextPrepare.call(options)) return cls(options) @staticmethod diff --git a/castle/commands/end_impersonation.py b/castle/commands/end_impersonation.py index 8d631f3..cb9f9fc 100644 --- a/castle/commands/end_impersonation.py +++ b/castle/commands/end_impersonation.py @@ -14,7 +14,6 @@ def call(self, options): context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) - ValidatorsPresent.call(context, 'ip') if context: options.update({'context': context}) diff --git a/castle/commands/start_impersonation.py b/castle/commands/start_impersonation.py index 8ba3fab..8d43c02 100644 --- a/castle/commands/start_impersonation.py +++ b/castle/commands/start_impersonation.py @@ -14,7 +14,6 @@ def call(self, options): context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) - ValidatorsPresent.call(context, 'ip') if context: options.update({'context': context}) diff --git a/castle/context/get_default.py b/castle/context/get_default.py index 7f866b1..c9a54cb 100644 --- a/castle/context/get_default.py +++ b/castle/context/get_default.py @@ -1,23 +1,14 @@ from castle.version import VERSION -from castle.headers.filter import HeadersFilter -from castle.fingerprint.extract import FingerprintExtract -from castle.headers.extract import HeadersExtract -from castle.ips.extract import IPsExtract __version__ = VERSION class ContextGetDefault(object): - def __init__(self, request, cookies): - self.cookies = self._fetch_cookies(request, cookies) - self.pre_headers = HeadersFilter(request).call() - def call(self): + @staticmethod + def call(): context = dict({ - 'fingerprint': self._fingerprint(), 'active': True, - 'headers': self._headers(), - 'ip': self._ip(), 'library': { 'name': 'castle-python', 'version': __version__ @@ -25,20 +16,3 @@ def call(self): }) return context - - def _ip(self): - return IPsExtract(self.pre_headers).call() - - def _fingerprint(self): - return FingerprintExtract(self.pre_headers, self.cookies).call() - - def _headers(self): - return HeadersExtract(self.pre_headers).call() - - @staticmethod - def _fetch_cookies(request, cookies): - if cookies: - return cookies - if hasattr(request, 'COOKIES') and request.COOKIES: - return request.COOKIES - return None diff --git a/castle/context/prepare.py b/castle/context/prepare.py index 5545cfe..854feaf 100644 --- a/castle/context/prepare.py +++ b/castle/context/prepare.py @@ -5,9 +5,8 @@ class ContextPrepare(object): @staticmethod - def call(request, options=None): + def call(options=None): if options is None: options = {} - default_context = ContextGetDefault( - request, options.get('cookies')).call() + default_context = ContextGetDefault.call() return ContextMerge.call(default_context, options.get('context', {})) diff --git a/castle/payload/prepare.py b/castle/payload/prepare.py index 03f74aa..e1f72ed 100644 --- a/castle/payload/prepare.py +++ b/castle/payload/prepare.py @@ -8,11 +8,11 @@ class PayloadPrepare(object): @staticmethod - def call(payload_options, request, options=None): + def call(payload_options, _request, options=None): if options is None: options = {} - context = ContextPrepare.call(request, UtilsMerge.call(payload_options, options)) + context = ContextPrepare.call(UtilsMerge.call(payload_options, options)) payload_options.setdefault('context', context) payload_options.setdefault('timestamp', generate_timestamp.call()) diff --git a/castle/test/client_test.py b/castle/test/client_test.py index ea82f0b..c7c6b81 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -30,13 +30,6 @@ def tearDown(self): def test_init(self): context = { 'active': True, - 'fingerprint': '1234', - 'headers': { - 'User-Agent': 'test', - 'X-Forwarded-For': '217.144.192.112', - 'X-Castle-Client-Id': '1234' - }, - 'ip': '217.144.192.112', 'library': {'name': 'castle-python', 'version': VERSION} } client = Client.from_request(request(), {}) diff --git a/castle/test/commands/end_impersonation_test.py b/castle/test/commands/end_impersonation_test.py index 2ecdfc7..05cd2ec 100644 --- a/castle/test/commands/end_impersonation_test.py +++ b/castle/test/commands/end_impersonation_test.py @@ -64,11 +64,3 @@ def test_call_no_event(self): with self.assertRaises(InvalidParametersError): CommandsEndImpersonation(context).call(options) - - def test_call_no_context_ip(self): - context = {} - options = default_options() - options['context'].pop('ip') - - with self.assertRaises(InvalidParametersError): - CommandsEndImpersonation(context).call(options) diff --git a/castle/test/commands/start_impersonation_test.py b/castle/test/commands/start_impersonation_test.py index 701f325..7c2bc54 100644 --- a/castle/test/commands/start_impersonation_test.py +++ b/castle/test/commands/start_impersonation_test.py @@ -64,11 +64,3 @@ def test_call_no_event(self): with self.assertRaises(InvalidParametersError): CommandsStartImpersonation(context).call(options) - - def test_call_no_context_ip(self): - context = {} - options = default_options() - options['context'].pop('ip') - - with self.assertRaises(InvalidParametersError): - CommandsStartImpersonation(context).call(options) diff --git a/castle/test/context/get_default_test.py b/castle/test/context/get_default_test.py index 939d64b..458968e 100644 --- a/castle/test/context/get_default_test.py +++ b/castle/test/context/get_default_test.py @@ -4,51 +4,11 @@ from castle.context.get_default import ContextGetDefault -def fingerprint(): - return 'abcd' - - -def cookies(): - return {'__cid': fingerprint()} - - -def request_ip(): - return '5.5.5.5' - - -def environ(): - return { - 'HTTP_X_FORWARDED_FOR': request_ip(), - 'HTTP_COOKIE': "__cid={fingerprint()};other=efgh", - 'HTTP-Accept-Language': 'en', - 'HTTP-User-Agent': 'test' - } - - -def request(env): - req = mock.Mock() - req.ip = request_ip() - req.environ = env - return req - - class ContextGetDefaultTestCase(unittest.TestCase): def test_default_context(self): - context = ContextGetDefault( - request(environ()), cookies()).call() - self.assertEqual(context['fingerprint'], fingerprint()) + context = ContextGetDefault.call() self.assertEqual(context['active'], True) - self.assertEqual( - context['headers'], - { - 'X-Forwarded-For': request_ip(), - 'Accept-Language': 'en', - 'User-Agent': 'test', - 'Cookie': True - } - ) - self.assertEqual(context['ip'], request_ip()) self.assertDictEqual( context['library'], {'name': 'castle-python', 'version': __version__} diff --git a/castle/test/context/prepare_test.py b/castle/test/context/prepare_test.py index b1502c7..48afede 100644 --- a/castle/test/context/prepare_test.py +++ b/castle/test/context/prepare_test.py @@ -19,20 +19,7 @@ class ContextPrepareTestCase(unittest.TestCase): def test_call(self): context = { 'active': True, - 'fingerprint': '1234', - 'headers': { - 'User-Agent': 'test', - 'X-Forwarded-For': '217.144.192.112', - 'X-Castle-Client-Id': '1234' - }, - 'ip': '217.144.192.112', 'library': {'name': 'castle-python', 'version': VERSION} } - result_context = ContextPrepare.call(request(), {}) + result_context = ContextPrepare.call({}) self.assertEqual(result_context, context) - - def test_setup_fingerprint_from_cookies(self): - cookies = {'__cid': '1234'} - options = {'cookies': cookies} - result_context = ContextPrepare.call(request(), options) - self.assertEqual(result_context['fingerprint'], '1234') diff --git a/castle/test/payload/prepare_test.py b/castle/test/payload/prepare_test.py index 0828132..66b5b8f 100644 --- a/castle/test/payload/prepare_test.py +++ b/castle/test/payload/prepare_test.py @@ -17,13 +17,6 @@ def request(): def ctx(): return { 'active': True, - 'fingerprint': '1234', - 'headers': { - 'User-Agent': 'test', - 'X-Castle-Client-Id': '1234', - 'X-Forwarded-For': '217.144.192.112' - }, - 'ip': '217.144.192.112', 'library': {'name': 'castle-python', 'version': VERSION} } From 008b7f7e1b2a6d5f54614714798febc99a6a2930 Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 16:21:34 +0100 Subject: [PATCH 06/12] Add options merge test --- castle/options/merge.py | 11 +++++++++++ castle/test/__init__.py | 4 +++- castle/test/options/merge_test.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 castle/options/merge.py create mode 100644 castle/test/options/merge_test.py diff --git a/castle/options/merge.py b/castle/options/merge.py new file mode 100644 index 0000000..4d088db --- /dev/null +++ b/castle/options/merge.py @@ -0,0 +1,11 @@ +from castle.utils.merge import UtilsMerge +from castle.utils.clone import UtilsClone + + +class OptionsMerge(object): + + @staticmethod + def call(initial_options, request_options): + source_copy = UtilsClone.call(initial_options) + UtilsMerge.call(source_copy, request_options) + return source_copy diff --git a/castle/test/__init__.py b/castle/test/__init__.py index bd05158..efb9d12 100644 --- a/castle/test/__init__.py +++ b/castle/test/__init__.py @@ -10,7 +10,6 @@ 'castle.test.api.get_devices_for_user_test', 'castle.test.api.report_device_test', 'castle.test.api_request_test', - 'castle.test.fingerprint.extract_test', 'castle.test.client_test', 'castle.test.command_test', 'castle.test.commands.approve_device_test', @@ -31,11 +30,14 @@ 'castle.test.core.send_request_test', 'castle.test.failover.prepare_response_test', 'castle.test.failover.strategy_test', + 'castle.test.fingerprint.extract_test', 'castle.test.headers.extract_test', 'castle.test.headers.filter_test', 'castle.test.headers.format_test', 'castle.test.ips.extract_test', 'castle.test.logger_test', + 'castle.test.get_default_test', + 'castle.test.options.merge_test', 'castle.test.payload.prepare_test', 'castle.test.secure_mode_test', 'castle.test.session_test', diff --git a/castle/test/options/merge_test.py b/castle/test/options/merge_test.py new file mode 100644 index 0000000..ef71792 --- /dev/null +++ b/castle/test/options/merge_test.py @@ -0,0 +1,13 @@ +from castle.test import unittest +from castle.options.merge import OptionsMerge + + +class OptionsMergeTestCase(unittest.TestCase): + + def test_call(self): + params = {'foo': {'foo': 'bar', 'nonfoo': 'nonbar'}, 'to_remove': 'ok'} + self.assertEqual( + OptionsMerge.call( + params, {'foo': {'foo': 'foo'}, 'to_remove': None}), + {'foo': {'foo': 'foo', 'nonfoo': 'nonbar'}} + ) From 01bc7d46aa9eb95323e56b4058b53dd7deaf06ab Mon Sep 17 00:00:00 2001 From: marysieek Date: Fri, 5 Mar 2021 16:26:10 +0100 Subject: [PATCH 07/12] Add default opts test --- castle/test/__init__.py | 2 +- castle/test/options/get_default_test.py | 50 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 castle/test/options/get_default_test.py diff --git a/castle/test/__init__.py b/castle/test/__init__.py index efb9d12..eeb637d 100644 --- a/castle/test/__init__.py +++ b/castle/test/__init__.py @@ -36,7 +36,7 @@ 'castle.test.headers.format_test', 'castle.test.ips.extract_test', 'castle.test.logger_test', - 'castle.test.get_default_test', + 'castle.test.options.get_default_test', 'castle.test.options.merge_test', 'castle.test.payload.prepare_test', 'castle.test.secure_mode_test', diff --git a/castle/test/options/get_default_test.py b/castle/test/options/get_default_test.py new file mode 100644 index 0000000..20f6eff --- /dev/null +++ b/castle/test/options/get_default_test.py @@ -0,0 +1,50 @@ +from castle.test import unittest, mock + +from castle.version import VERSION as __version__ +from castle.options.get_default import OptionsGetDefault + + +def fingerprint(): + return 'abcd' + + +def cookies(): + return {'__cid': fingerprint()} + + +def request_ip(): + return '5.5.5.5' + + +def environ(): + return { + 'HTTP_X_FORWARDED_FOR': request_ip(), + 'HTTP_COOKIE': "__cid={fingerprint()};other=efgh", + 'HTTP-Accept-Language': 'en', + 'HTTP-User-Agent': 'test' + } + + +def request(env): + req = mock.Mock() + req.ip = request_ip() + req.environ = env + return req + + +class OptionsGetDefaultTestCase(unittest.TestCase): + + def test_default_context(self): + opts = OptionsGetDefault( + request(environ()), cookies()).call() + self.assertEqual(opts['fingerprint'], fingerprint()) + self.assertEqual( + opts['headers'], + { + 'X-Forwarded-For': request_ip(), + 'Accept-Language': 'en', + 'User-Agent': 'test', + 'Cookie': True + } + ) + self.assertEqual(opts['ip'], request_ip()) From 0fc65d351a6d04a60a9fcd0ea20bf45588e3fd45 Mon Sep 17 00:00:00 2001 From: marysieek Date: Mon, 8 Mar 2021 10:48:53 +0100 Subject: [PATCH 08/12] Update from_request --- castle/client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/castle/client.py b/castle/client.py index 64b4a52..57a721e 100644 --- a/castle/client.py +++ b/castle/client.py @@ -5,6 +5,8 @@ from castle.commands.track import CommandsTrack from castle.configuration import configuration from castle.context.prepare import ContextPrepare +from castle.options.merge import OptionsMerge +from castle.options.get_default import OptionsGetDefault from castle.errors import InternalServerError, RequestError, ImpersonationFailed from castle.failover.prepare_response import FailoverPrepareResponse from castle.failover.strategy import FailoverStrategy @@ -13,12 +15,12 @@ class Client(object): @classmethod - def from_request(cls, _request, options=None): - if options is None: - options = {} + def from_request(cls, request, options={}): + default_options = OptionsGetDefault(request, options.get('cookies')).call() + options_with_default_opts = OptionsMerge.call(options, default_options) - options.setdefault('context', ContextPrepare.call(options)) - return cls(options) + options_with_default_opts.setdefault('context', ContextPrepare.call(options)) + return cls(options_with_default_opts) @staticmethod def failover_response_or_raise(options, exception): @@ -28,9 +30,7 @@ def failover_response_or_raise(options, exception): options.get('user_id'), None, exception.__class__.__name__ ).call() - def __init__(self, options=None): - if options is None: - options = {} + def __init__(self, options={}): self.do_not_track = options.get('do_not_track', False) self.timestamp = options.get('timestamp') self.context = options.get('context') From 9a51a46c6225740a5c97911beee3e2aa746cb342 Mon Sep 17 00:00:00 2001 From: marysieek Date: Mon, 8 Mar 2021 10:53:10 +0100 Subject: [PATCH 09/12] Update client.py --- castle/client.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/castle/client.py b/castle/client.py index 57a721e..63663d9 100644 --- a/castle/client.py +++ b/castle/client.py @@ -31,6 +31,7 @@ def failover_response_or_raise(options, exception): ).call() def __init__(self, options={}): + self.default_options = options self.do_not_track = options.get('do_not_track', False) self.timestamp = options.get('timestamp') self.context = options.get('context') @@ -41,41 +42,51 @@ def _add_timestamp_if_necessary(self, options): options.setdefault('timestamp', self.timestamp) def authenticate(self, options): + options_with_default_opts = OptionsMerge.call(options, self.default_options) + if self.tracked(): - self._add_timestamp_if_necessary(options) - command = CommandsAuthenticate(self.context).call(options) + self._add_timestamp_if_necessary(options_with_default_opts) + command = CommandsAuthenticate(self.context).call(options_with_default_opts) try: response = self.api.call(command) response.update(failover=False, failover_reason=None) return response except (RequestError, InternalServerError) as exception: - return Client.failover_response_or_raise(options, exception) + return Client.failover_response_or_raise(options_with_default_opts, exception) else: return FailoverPrepareResponse( - options.get('user_id'), + options_with_default_opts.get('user_id'), 'allow', 'Castle set to do not track.' ).call() def start_impersonation(self, options): - self._add_timestamp_if_necessary(options) - response = self.api.call(CommandsStartImpersonation(self.context).call(options)) + options_with_default_opts = OptionsMerge.call(options, self.default_options) + + self._add_timestamp_if_necessary(options_with_default_opts) + response = self.api.call(CommandsStartImpersonation( + self.context).call(options_with_default_opts)) if not response.get('success'): raise ImpersonationFailed return response def end_impersonation(self, options): - self._add_timestamp_if_necessary(options) - response = self.api.call(CommandsEndImpersonation(self.context).call(options)) + options_with_default_opts = OptionsMerge.call(options, self.default_options) + + self._add_timestamp_if_necessary(options_with_default_opts) + response = self.api.call(CommandsEndImpersonation( + self.context).call(options_with_default_opts)) if not response.get('success'): raise ImpersonationFailed return response def track(self, options): + options_with_default_opts = OptionsMerge.call(options, self.default_options) + if not self.tracked(): return None - self._add_timestamp_if_necessary(options) - return self.api.call(CommandsTrack(self.context).call(options)) + self._add_timestamp_if_necessary(options_with_default_opts) + return self.api.call(CommandsTrack(self.context).call(options_with_default_opts)) def disable_tracking(self): self.do_not_track = True From 81d9fea73cb060e22f25dca6370c77aa7edd4f2b Mon Sep 17 00:00:00 2001 From: marysieek Date: Mon, 8 Mar 2021 11:03:40 +0100 Subject: [PATCH 10/12] Update commands --- castle/commands/end_impersonation.py | 2 +- castle/commands/start_impersonation.py | 2 +- castle/test/commands/end_impersonation_test.py | 13 +++++++++++-- castle/test/commands/start_impersonation_test.py | 13 +++++++++++-- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/castle/commands/end_impersonation.py b/castle/commands/end_impersonation.py index cb9f9fc..dd006e6 100644 --- a/castle/commands/end_impersonation.py +++ b/castle/commands/end_impersonation.py @@ -10,7 +10,7 @@ def __init__(self, context): self.context = context def call(self, options): - ValidatorsPresent.call(options, 'user_id') + ValidatorsPresent.call(options, 'user_id', 'headers') context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) diff --git a/castle/commands/start_impersonation.py b/castle/commands/start_impersonation.py index 8d43c02..909e22e 100644 --- a/castle/commands/start_impersonation.py +++ b/castle/commands/start_impersonation.py @@ -10,7 +10,7 @@ def __init__(self, context): self.context = context def call(self, options): - ValidatorsPresent.call(options, 'user_id') + ValidatorsPresent.call(options, 'user_id', 'headers') context = ContextMerge.call(self.context, options.get('context')) context = ContextSanitize.call(context) diff --git a/castle/test/commands/end_impersonation_test.py b/castle/test/commands/end_impersonation_test.py index 05cd2ec..ad669ca 100644 --- a/castle/test/commands/end_impersonation_test.py +++ b/castle/test/commands/end_impersonation_test.py @@ -8,7 +8,8 @@ def default_options(): """Default options include all required fields.""" return {'properties': {'impersonator': 'admin'}, 'user_id': '1234', - 'context': {'ip': '127.0.0.1'}} + 'context': {'ip': '127.0.0.1'}, + 'headers': {'random': 'header'}} def default_options_plus(**extra): @@ -57,10 +58,18 @@ def test_call(self): self.assertEqual(CommandsEndImpersonation(context).call(options), expected) - def test_call_no_event(self): + def test_call_no_user_id(self): context = {} options = default_options() options.pop('user_id') with self.assertRaises(InvalidParametersError): CommandsEndImpersonation(context).call(options) + + def test_call_no_headers(self): + context = {} + options = default_options() + options.pop('headers') + + with self.assertRaises(InvalidParametersError): + CommandsEndImpersonation(context).call(options) diff --git a/castle/test/commands/start_impersonation_test.py b/castle/test/commands/start_impersonation_test.py index 7c2bc54..106cdd9 100644 --- a/castle/test/commands/start_impersonation_test.py +++ b/castle/test/commands/start_impersonation_test.py @@ -8,7 +8,8 @@ def default_options(): """Default options include all required fields.""" return {'properties': {'impersonator': 'admin'}, 'user_id': '1234', - 'context': {'ip': '127.0.0.1', 'user_agent': 'Chrome'}} + 'context': {'ip': '127.0.0.1', 'user_agent': 'Chrome'}, + 'headers': {'random': 'header'}} def default_options_plus(**extra): @@ -57,10 +58,18 @@ def test_call(self): self.assertEqual(CommandsStartImpersonation(context).call(options), expected) - def test_call_no_event(self): + def test_call_no_user_id(self): context = {} options = default_options() options.pop('user_id') with self.assertRaises(InvalidParametersError): CommandsStartImpersonation(context).call(options) + + def test_call_no_headers(self): + context = {} + options = default_options() + options.pop('headers') + + with self.assertRaises(InvalidParametersError): + CommandsStartImpersonation(context).call(options) From e603300ec3fd7a7636a4bf483e94827e3a14f610 Mon Sep 17 00:00:00 2001 From: marysieek Date: Mon, 8 Mar 2021 11:10:13 +0100 Subject: [PATCH 11/12] Add default options specs --- castle/client.py | 10 ++++++++-- castle/test/client_test.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/castle/client.py b/castle/client.py index 63663d9..b09954f 100644 --- a/castle/client.py +++ b/castle/client.py @@ -15,7 +15,10 @@ class Client(object): @classmethod - def from_request(cls, request, options={}): + def from_request(cls, request, options=None): + if options is None: + options = {} + default_options = OptionsGetDefault(request, options.get('cookies')).call() options_with_default_opts = OptionsMerge.call(options, default_options) @@ -30,7 +33,10 @@ def failover_response_or_raise(options, exception): options.get('user_id'), None, exception.__class__.__name__ ).call() - def __init__(self, options={}): + def __init__(self, options=None): + if options is None: + options = {} + self.default_options = options self.do_not_track = options.get('do_not_track', False) self.timestamp = options.get('timestamp') diff --git a/castle/test/client_test.py b/castle/test/client_test.py index c7c6b81..a7c4c79 100644 --- a/castle/test/client_test.py +++ b/castle/test/client_test.py @@ -33,6 +33,17 @@ def test_init(self): 'library': {'name': 'castle-python', 'version': VERSION} } client = Client.from_request(request(), {}) + default_options = { + 'fingerprint': '1234', + 'headers': { + 'X-Forwarded-For': '217.144.192.112', + 'User-Agent': 'test', + 'X-Castle-Client-Id': '1234' + }, + 'ip': '217.144.192.112', + 'context': {'active': True, 'library': {'name': 'castle-python', 'version': '5.0.1'}} + } + self.assertEqual(client.default_options, default_options) self.assertEqual(client.do_not_track, False) self.assertEqual(client.context, context) self.assertIsInstance(client.api, APIRequest) From dbaa7a719f5f97e740c820ca766336af9a7c6504 Mon Sep 17 00:00:00 2001 From: marysieek Date: Mon, 8 Mar 2021 11:24:40 +0100 Subject: [PATCH 12/12] Add payload prepare test --- castle/payload/prepare.py | 19 ++++++++------ castle/test/payload/prepare_test.py | 39 ++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/castle/payload/prepare.py b/castle/payload/prepare.py index e1f72ed..88ff83d 100644 --- a/castle/payload/prepare.py +++ b/castle/payload/prepare.py @@ -2,22 +2,27 @@ from castle.utils.timestamp import UtilsTimestamp as generate_timestamp from castle.context.prepare import ContextPrepare -from castle.utils.merge import UtilsMerge +from castle.options.get_default import OptionsGetDefault +from castle.options.merge import OptionsMerge class PayloadPrepare(object): @staticmethod - def call(payload_options, _request, options=None): + def call(payload_options, request, options=None): if options is None: options = {} - context = ContextPrepare.call(UtilsMerge.call(payload_options, options)) + default_options = OptionsGetDefault(request, options.get('cookies')).call() + options_with_default_opts = OptionsMerge.call(options, default_options) + options_for_payload = OptionsMerge.call(payload_options, options_with_default_opts) - payload_options.setdefault('context', context) - payload_options.setdefault('timestamp', generate_timestamp.call()) + context = ContextPrepare.call(options_for_payload) - if 'traits' in payload_options: + options_for_payload.setdefault('context', context) + options_for_payload.setdefault('timestamp', generate_timestamp.call()) + + if 'traits' in options_for_payload: warnings.warn('use user_traits instead of traits key', DeprecationWarning) - return payload_options + return options_for_payload diff --git a/castle/test/payload/prepare_test.py b/castle/test/payload/prepare_test.py index 66b5b8f..5329e02 100644 --- a/castle/test/payload/prepare_test.py +++ b/castle/test/payload/prepare_test.py @@ -21,6 +21,37 @@ def ctx(): } +def res(): + return { + 'foo': 'bar', + 'fingerprint': '1234', + 'headers': { + 'X-Forwarded-For': '217.144.192.112', + 'User-Agent': 'test', + 'X-Castle-Client-Id': '1234' + }, + 'ip': '217.144.192.112', + 'context': {'active': True, 'library': {'name': 'castle-python', 'version': '5.0.1'}}, + 'timestamp': '2018-01-02T03:04:05.678' + } + + +def resWithDeprecation(): + return { + 'foo': 'bar', + 'traits': {}, + 'fingerprint': '1234', + 'headers': { + 'X-Forwarded-For': '217.144.192.112', + 'User-Agent': 'test', + 'X-Castle-Client-Id': '1234' + }, + 'ip': '217.144.192.112', + 'context': {'active': True, 'library': {'name': 'castle-python', 'version': '5.0.1'}}, + 'timestamp': '2018-01-02T03:04:05.678' + } + + class ContextPrepareTestCase(unittest.TestCase): def setUp(self): # patch timestamp to return a known value @@ -31,10 +62,10 @@ def setUp(self): def test_call(self): options = PayloadPrepare.call({'foo': 'bar'}, request()) - self.assertEqual( - options, {'foo': 'bar', 'timestamp': '2018-01-02T03:04:05.678', 'context': ctx()}) + + self.assertEqual(options, res()) def test_call_with_deprecation(self): options = PayloadPrepare.call({'foo': 'bar', 'traits': {}}, request()) - self.assertEqual( - options, {'foo': 'bar', 'timestamp': '2018-01-02T03:04:05.678', 'traits': {}, 'context': ctx()}) + + self.assertEqual(options, resWithDeprecation())