diff --git a/.gitignore b/.gitignore index 0069152..cf6b4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ /build .coverage /htmlcov -.vscode/ \ No newline at end of file +.vscode/ +.idea/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..699acd5 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,168 @@ +master +------ + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- `#79 `__ drop the configuration ``host``, ``port``, ``url_prefix`` options in favor of ``base_url`` + +4.0.0 (2020-07-06) +------------------ + +Features: +~~~~~~~~~ + +- `#69 `__ added + impersonator to properties + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- `#70 `__ dropped + blacklist and whitelist support, in favour of denylist and allowlist + +3.3.0 (2020-05-22) +------------------ + +- `#67 `__ add + ``trusted_proxy_depth`` and ``trust_proxy_chain`` configuration + options + +3.2.0 (2020-02-31) +------------------ + +- `#64 `__ dropped + X-Client-Id from calculation of ip, drop appending default ip headers + to the ip\_header list config when config is provided (in that case + default headers have to explicitly provided) + +3.1.0 (2020-02-27) +------------------ + +- `#61 `__ improve + headers and ip extractions, improve ip\_headers config, add trusted + proxies config, added more events to events list +- `#62 `__ move + request,response, session to apis namespace, add config check before + doing request + +3.0.0 (2020-02-13) +------------------ + +- `#59 `__ drop + requests min version in ci +- `#56 `__ drop + special ip header behavior +- `#58 `__ Adds + ``ip_header`` configuration option + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- `#57 `__ dropped + support for python 2 + +2.4.0 (2019-11-20) +------------------ + +- `#53 `__ update + whitelisting and blacklisting behavior + +2.3.1 (2019-04-05) +------------------ + +- `#50 `__ generate + new default timestamps for each call + +2.3.0 (2019-01-16) +------------------ + +- `#48 `__ add + connection pooling +- `#47 `__ add event + constants +- `#40 `__ remove + requirement for ``user_id`` + +2.2.1 (2018-09-04) +------------------ + +- `#41 `__ add python + 2.6, python 3.7 + +2.2.0 (2018-04-18) +------------------ + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- `#35 `__ usage of + ``traits`` key is deprecated, use ``user_traits`` instead +- `#38 `__ make api + related errors inherit from ``ApiError`` +- `#38 `__ rename + ``FailoverStrategyValueError`` to ``ConfigurationError`` + +Enhancements: +~~~~~~~~~~~~~ + +- `#37 `__ + ``X-Castle-Client-Id`` takes precedence over ``cid`` from ``cookies`` +- `#36 `__ raise + ``ImpersonationFailed`` when impersonation request failed + +2.1.1 (2018-02-26) +------------------ + +Features: +~~~~~~~~~ + +- add reset option to impersonation + +2.1.0 (2018-02-09) +------------------ + +Features: +~~~~~~~~~ + +- add support for impersonation + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- switched configuration request\_timeout from seconds to milliseconds + +2.0.0 (2018-02-09) +------------------ + +Features: +~~~~~~~~~ + +- code reorganization +- added ``Client.to_context`` method which allows to generate context + object from the request +- additional timestamp and sent\_at time values are automatically added + to the requests +- when data is sent in batches you may want to wrap data options with + ``Client.to_options`` method before you send it to the worker (see + README) to include proper timestamp in the query +- added X-Forwarded-For and CF\_CONNECTING\_IP to whitelisted headers +- fetch IP from CF\_CONNECTING\_IP if possible + +Breaking Changes: +~~~~~~~~~~~~~~~~~ + +- Client does not build context object anymore to use previous + functionality use ``Client.from_request`` +- code reorganization + +1.0.1 (2017-12-08) +------------------ + +- Handle cookies from Django request + +1.0.0 (2017-10-16) +------------------ + +- Initial release + diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index f054962..0000000 --- a/HISTORY.md +++ /dev/null @@ -1,97 +0,0 @@ -## master - -## 4.0.0 (2020-07-06) - -### Features: -- [#69](https://github.com/castle/castle-python/pull/69/files) added impersonator to properties - -### Breaking Changes: -- [#70](https://github.com/castle/castle-python/pull/70) dropped blacklist and whitelist support, in favour of denylist and allowlist - -## 3.3.0 (2020-05-22) - -- [#67](https://github.com/castle/castle-python/pull/67) add `trusted_proxy_depth` and `trust_proxy_chain` configuration options - -## 3.2.0 (2020-02-31) - -- [#64](https://github.com/castle/castle-python/pull/64) dropped X-Client-Id from calculation of ip, drop appending default ip headers to the ip_header list config when config is provided (in that case default headers have to explicitly provided) - -## 3.1.0 (2020-02-27) - -- [#61](https://github.com/castle/castle-python/pull/61) improve headers and ip extractions, improve ip_headers config, add trusted proxies config, added more events to events list -- [#62](https://github.com/castle/castle-python/pull/62) move request,response, session to apis namespace, add config check before doing request - -## 3.0.0 (2020-02-13) - -- [#59](https://github.com/castle/castle-python/pull/59) drop requests min version in ci -- [#56](https://github.com/castle/castle-python/pull/56) drop special ip header behavior -- [#58](https://github.com/castle/castle-python/pull/58) Adds `ip_header` configuration option - -### Breaking Changes: - -- [#57](https://github.com/castle/castle-python/pull/57) dropped support for python 2 - -## 2.4.0 (2019-11-20) - -- [#53](https://github.com/castle/castle-python/pull/53) update whitelisting and blacklisting behavior - -## 2.3.1 (2019-04-05) - -- [#50](https://github.com/castle/castle-python/pull/50) generate new default timestamps for each call - -## 2.3.0 (2019-01-16) - -- [#48](https://github.com/castle/castle-python/pull/48) add connection pooling -- [#47](https://github.com/castle/castle-python/pull/47) add event constants -- [#40](https://github.com/castle/castle-python/pull/40) remove requirement for `user_id` - -## 2.2.1 (2018-09-04) - -- [#41](https://github.com/castle/castle-python/pull/41) add python 2.6, python 3.7 - -## 2.2.0 (2018-04-18) - -### Breaking Changes: - -- [#35](https://github.com/castle/castle-python/pull/35) usage of `traits` key is deprecated, use `user_traits` instead -- [#38](https://github.com/castle/castle-python/pull/38) make api related errors inherit from `ApiError` -- [#38](https://github.com/castle/castle-python/pull/38) rename `FailoverStrategyValueError` to `ConfigurationError` - -### Enhancements: - -- [#37](https://github.com/castle/castle-python/pull/37) `X-Castle-Client-Id` takes precedence over `cid` from `cookies` -- [#36](https://github.com/castle/castle-python/pull/36) raise `ImpersonationFailed` when impersonation request failed - -## 2.1.1 (2018-02-26) - -### Features: -- add reset option to impersonation - -## 2.1.0 (2018-02-09) - -### Features: -- add support for impersonation - -### Breaking Changes: -- switched configuration request_timeout from seconds to milliseconds - -## 2.0.0 (2018-02-09) - -### Features: -- code reorganization -- added `Client.to_context` method which allows to generate context object from the request -- additional timestamp and sent_at time values are automatically added to the requests -- when data is sent in batches you may want to wrap data options with `Client.to_options` method before you send it to the worker (see README) to include proper timestamp in the query -- added X-Forwarded-For and CF_CONNECTING_IP to whitelisted headers -- fetch IP from CF_CONNECTING_IP if possible - -### Breaking Changes: -- Client does not build context object anymore to use previous functionality use `Client.from_request` -- code reorganization - -## 1.0.1 (2017-12-08) -* Handle cookies from Django request - -## 1.0.0 (2017-10-16) - -* Initial release diff --git a/README.rst b/README.rst index beca6c1..795473a 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,9 @@ 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 + # Base Castle API url + # configuration.base_url = "https://api.castle.io/v1" + # 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. diff --git a/RELEASING.md b/RELEASING.md deleted file mode 100644 index 6b0594e..0000000 --- a/RELEASING.md +++ /dev/null @@ -1,25 +0,0 @@ -Releasing -========= - -1. Create branch `X.Y.Z`. -2. Update `VERSION` in `castle/version.py` to the new version -3. Update the `HISTORY.md` for the impending release -4. `git commit -am "release X.Y.Z"` (where X.Y.Z is the new version) -5. Push to Github, make PR, and when ok, merge. -6. Make a release on Github, specify tag as `vX.Y.Z` to create a tag. -7. `git checkout master && git pull` -8. `rm -rf dist` -9. `python3 setup.py sdist bdist_wheel` -10. `twine upload dist/*` - - -When you change something in the README.rst make sure it is in the correct format, as pypi -will ignore the file if it is not valid. - -`pip3 install collective.checkdocs` - -`pip3 install pygments` - -`python3 setup.py checkdocs` - -To upload to testpypi `twine upload --repository-url https://test.pypi.org/legacy/ dist/*` diff --git a/RELEASING.rst b/RELEASING.rst new file mode 100644 index 0000000..dc6aedd --- /dev/null +++ b/RELEASING.rst @@ -0,0 +1,26 @@ +Releasing +========= + +#. Create release branch ``X.Y.Z`` from ``develop``. +#. Update ``VERSION`` in ``castle/version.py`` to the new version +#. Update the ``CHANGELOG.rst`` for the impending release +#. ``git commit -am "release X.Y.Z"`` (where X.Y.Z is the new version) +#. Push to Github, make PR to the develop branch, and when approved, merge. +#. Pull latest ``develop``, merge it to ``master``, and push it. +#. Make a release on Github from the ``master`` branch, specify tag as ``vX.Y.Z`` to create a tag. +#. ``git checkout master && git pull`` +#. ``rm -rf dist`` +#. ``python3 setup.py sdist bdist_wheel`` +#. ``twine upload dist/*`` + +When you change something in the README.rst make sure it is in the +correct format, as pypi will ignore the file if it is not valid. + +``pip3 install collective.checkdocs`` + +``pip3 install pygments`` + +``python3 setup.py checkdocs`` + +To upload to testpypi +``twine upload --repository-url https://test.pypi.org/legacy/ dist/*`` diff --git a/castle/apis/request.py b/castle/apis/request.py index 4bf56c4..bab8556 100644 --- a/castle/apis/request.py +++ b/castle/apis/request.py @@ -2,6 +2,7 @@ from castle.configuration import configuration from castle.apis.session import ApisSession +HTTPS_SCHEME = 'https' class ApisRequest(object): def __init__(self, headers=None): @@ -25,17 +26,8 @@ def build_url(self, path): @staticmethod def build_base_url(): - template = 'http://{host}:{port}/{prefix}' - - if configuration.port == 443: - template = 'https://{host}/{prefix}' - - return template.format( - host=configuration.host.strip('/'), - port=configuration.port, - prefix=configuration.url_prefix.strip('/') - ) + return configuration.base_url.geturl() @staticmethod def verify(): - return configuration.port == 443 + return configuration.base_url.scheme == HTTPS_SCHEME diff --git a/castle/configuration.py b/castle/configuration.py index 6875ec4..f1bb397 100644 --- a/castle/configuration.py +++ b/castle/configuration.py @@ -1,3 +1,4 @@ +from urllib.parse import urlparse, ParseResult from castle.exceptions import ConfigurationError from castle.headers_formatter import HeadersFormatter @@ -26,13 +27,13 @@ "X-Castle-Client-Id", ] +# API endpoint +BASE_URL = 'https://api.castle.io/v1' +FAILOVER_STRATEGY = 'allow' # 500 milliseconds REQUEST_TIMEOUT = 500 +# regexp of trusted proxies which is always appended to the trusted proxy list FAILOVER_STRATEGIES = ['allow', 'deny', 'challenge', 'throw'] -HOST = 'api.castle.io' -PORT = 443 -URL_PREFIX = '/v1' -FAILOVER_STRATEGY = 'allow' TRUSTED_PROXIES = [r""" \A127\.0\.0\.1\Z| \A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.| @@ -41,24 +42,21 @@ \Aunix\Z| \Aunix:"""] - class Configuration(object): def __init__(self): - self.api_secret = None - self.host = HOST - self.port = PORT - self.url_prefix = URL_PREFIX - self.allowlisted = [] - self.denylisted = [] self.request_timeout = REQUEST_TIMEOUT self.failover_strategy = FAILOVER_STRATEGY + self.base_url = urlparse(BASE_URL) + self.allowlisted = [] + self.denylisted = [] + self.api_secret = None self.ip_headers = [] self.trusted_proxies = [] self.trust_proxy_chain = False self.trusted_proxy_depth = None def isValid(self): - return self.host and self.port and self.api_secret + return self.api_secret and self.base_url.hostname @property def api_secret(self): @@ -69,28 +67,15 @@ def api_secret(self, value): self.__api_secret = value @property - def host(self): - return self.__host - - @host.setter - def host(self, value): - self.__host = value - - @property - def port(self): - return self.__port + def base_url(self): + return self.__base_url - @port.setter - def port(self, value): - self.__port = value - - @property - def url_prefix(self): - return self.__url_prefix - - @url_prefix.setter - def url_prefix(self, value): - self.__url_prefix = value + @base_url.setter + def base_url(self, value): + if isinstance(value, ParseResult): + self.__base_url = value + else: + self.__base_url = urlparse(value) @property def allowlisted(self): diff --git a/castle/test/apis/request_test.py b/castle/test/apis/request_test.py index de78085..79f9e40 100644 --- a/castle/test/apis/request_test.py +++ b/castle/test/apis/request_test.py @@ -56,15 +56,13 @@ def test_build_query(self): configuration.api_secret = None def test_connection_pooled(self): - configuration.host = 'localhost' - configuration.port = 65521 + configuration.base_url = 'http://localhost:65521' run_server() request = ApisRequest() data = {'event': '$login.authenticate', 'user_id': '12345'} response = request.build_query('post', 'authenticate', data) num_pools = len(response.connection.poolmanager.pools.keys()) - configuration.host = 'api.castle.io' - configuration.port = 443 + configuration.base_url = 'https://api.castle.io/v1' self.assertEqual(num_pools, 1) def test_build_url(self): @@ -73,10 +71,17 @@ def test_build_url(self): 'https://api.castle.io/v1/authenticate' ) + def test_build_url_with_port(self): + configuration.base_url = 'http://api.castle.local:3001' + self.assertEqual( + ApisRequest().build_url('test'), + 'http://api.castle.local:3001/test' + ) + def test_verify_true(self): self.assertEqual(ApisRequest().verify(), True) def test_verify_false(self): - configuration.port = 3001 + configuration.base_url = 'http://api.castle.io' self.assertEqual(ApisRequest().verify(), False) - configuration.port = 443 + configuration.base_url = 'https://api.castle.io/v1' diff --git a/castle/test/configuration_test.py b/castle/test/configuration_test.py index 12bfb92..4f4851e 100644 --- a/castle/test/configuration_test.py +++ b/castle/test/configuration_test.py @@ -1,15 +1,15 @@ +from urllib.parse import urlparse from castle.test import unittest from castle.exceptions import ConfigurationError from castle.configuration import Configuration - class ConfigurationTestCase(unittest.TestCase): def test_default_values(self): config = Configuration() + uri = urlparse('https://api.castle.io/v1') self.assertEqual(config.api_secret, None) - self.assertEqual(config.host, 'api.castle.io') - self.assertEqual(config.port, 443) - self.assertEqual(config.url_prefix, '/v1') + self.assertEqual(config.base_url, uri) + self.assertEqual(config.base_url.path, '/v1') self.assertEqual(config.allowlisted, []) self.assertEqual(config.denylisted, []) self.assertEqual(config.request_timeout, 500) @@ -22,20 +22,19 @@ def test_api_secret_setter(self): config.api_secret = 'test' self.assertEqual(config.api_secret, 'test') - def test_host_setter(self): - config = Configuration() - config.host = 'test' - self.assertEqual(config.host, 'test') - - def test_port_setter(self): + def test_base_url_setter(self): config = Configuration() - config.port = 80 - self.assertEqual(config.port, 80) + config.base_url = 'test' + self.assertEqual(config.base_url, urlparse('test')) - def test_url_prefix_setter(self): + def test_base_url_setter_with_port(self): config = Configuration() - config.url_prefix = '/v2' - self.assertEqual(config.url_prefix, '/v2') + local_api_url = 'http://api.castle.local:3001/v1' + config.base_url = local_api_url + parsed_url = urlparse(local_api_url) + self.assertEqual(config.base_url, parsed_url) + self.assertEqual(config.base_url.path, '/v1') + self.assertEqual(config.base_url.port, 3001) def test_allowlisted_setter_list(self): config = Configuration()