diff --git a/exasol/bucketfs/__init__.py b/exasol/bucketfs/__init__.py index f808a693..a3f56215 100644 --- a/exasol/bucketfs/__init__.py +++ b/exasol/bucketfs/__init__.py @@ -133,7 +133,6 @@ def __init__( Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path to a CA bundle to use. Defaults to ``True``. - """ self._url = _parse_service_url(url) self._authenticator = defaultdict( @@ -176,20 +175,36 @@ def __getitem__(self, item: str) -> "Bucket": class Bucket: - def __init__(self, name: str, service: str, username: str, password: str): + def __init__( + self, + name: str, + service: str, + username: str, + password: str, + verify: bool | str = True, + ): """ Create a new bucket instance. Args: - name: of the bucket. - service: url where this bucket is hosted on. - username: used for authentication. - password: used for authentication. + name: + Name of the bucket. + service: + Url where this bucket is hosted on. + username: + Username used for authentication. + password: + Password used for authentication. + verify: + Either a boolean, in which case it controls whether we verify + the server's TLS certificate, or a string, in which case it must be a path + to a CA bundle to use. Defaults to ``True``. """ self._name = name self._service = _parse_service_url(service) self._username = username self._password = password + self._verify = verify def __str__(self): return f"Bucket<{self.name} | on: {self._service}>" @@ -205,7 +220,7 @@ def _auth(self) -> HTTPBasicAuth: @property def files(self) -> Iterable[str]: url = _build_url(service_url=self._service, bucket=self.name) - response = requests.get(url, auth=self._auth) + response = requests.get(url, auth=self._auth, verify=self._verify) try: response.raise_for_status() except HTTPError as ex: @@ -228,7 +243,7 @@ def upload( data: raw content of the file. """ url = _build_url(service_url=self._service, bucket=self.name, path=path) - response = requests.put(url, data=data, auth=self._auth) + response = requests.put(url, data=data, auth=self._auth, verify=self._verify) try: response.raise_for_status() except HTTPError as ex: @@ -245,7 +260,7 @@ def delete(self, path) -> None: A BucketFsError if the operation couldn't be executed successfully. """ url = _build_url(service_url=self._service, bucket=self.name, path=path) - response = requests.delete(url, auth=self._auth) + response = requests.delete(url, auth=self._auth, verify=self._verify) try: response.raise_for_status() except HTTPError as ex: @@ -263,7 +278,9 @@ def download(self, path: str, chunk_size: int = 8192) -> Iterable[ByteString]: An iterable of binary chunks representing the downloaded file. """ url = _build_url(service_url=self._service, bucket=self.name, path=path) - with requests.get(url, stream=True, auth=self._auth) as response: + with requests.get( + url, stream=True, auth=self._auth, verify=self._verify + ) as response: try: response.raise_for_status() except HTTPError as ex: diff --git a/exasol/bucketfs/version.py b/exasol/bucketfs/version.py index 7ab25474..647d463f 100644 --- a/exasol/bucketfs/version.py +++ b/exasol/bucketfs/version.py @@ -1,5 +1,8 @@ # ATTENTION: -# This file is generated, do not edit it manually! +# This file is generated by exasol/toolbox/pre_commit_hooks/package_version.py when using: +# * either "poetry run nox -s fix" +# * or "poetry run version-check --fix" +# Do not edit this file manually! # If you need to change the version, do so in the project.toml, e.g. by using `poetry version X.Y.Z`. MAJOR = 0 MINOR = 9 diff --git a/test/integration/bucketfs_test.py b/test/integration/bucketfs_test.py index 20978b71..1cbe33ab 100644 --- a/test/integration/bucketfs_test.py +++ b/test/integration/bucketfs_test.py @@ -1,5 +1,6 @@ import random import string +from contextlib import contextmanager from inspect import cleandoc from typing import ( ByteString, @@ -20,9 +21,22 @@ Bucket, Service, as_bytes, + as_string, ) +@contextmanager +def does_not_raise(exception_type: Exception = Exception): + try: + yield + + except exception_type as ex: + raise AssertionError(f"Raised exception {ex} when it should not!") from ex + + except Exception as ex: + raise AssertionError(f"An unexpected exception {ex} raised.") from ex + + @pytest.mark.parametrize( "expected", [ @@ -156,3 +170,125 @@ def test_ssl_verification_for_bucketfs_service_can_be_bypassed(httpsserver): expected = ["default", "demo_foo", "demo_bar"] actual = [bucket for bucket in bucketfs] assert expected == actual + + +def test_ssl_verification_for_bucket_files_fails(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + ) + + with pytest.raises(requests.exceptions.SSLError) as execinfo: + _ = {file for file in bucket} + assert "CERTIFICATE_VERIFY_FAILED" in str(execinfo) + + +def test_ssl_verification_for_bucket_files_can_be_bypassed(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + verify=False, + ) + + with does_not_raise(requests.exceptions.SSLError): + _ = {file for file in bucket} + + +def test_ssl_verification_for_bucket_upload_fails(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + ) + + with pytest.raises(requests.exceptions.SSLError) as execinfo: + data = bytes([65, 65, 65, 65]) + bucket.upload("some/other/path/file2.bin", data) + assert "CERTIFICATE_VERIFY_FAILED" in str(execinfo) + + +def test_ssl_verification_for_bucket_upload_can_be_bypassed(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + verify=False, + ) + + with does_not_raise(requests.exceptions.SSLError): + data = bytes([65, 65, 65, 65]) + bucket.upload("some/other/path/file2.bin", data) + + +def test_ssl_verification_for_bucket_delete_fails(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + ) + + with pytest.raises(requests.exceptions.SSLError) as execinfo: + bucket.delete("some/other/path/file2.bin") + assert "CERTIFICATE_VERIFY_FAILED" in str(execinfo) + + +def test_ssl_verification_for_bucket_delete_can_be_bypassed(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + verify=False, + ) + + with does_not_raise(requests.exceptions.SSLError): + bucket.delete("some/other/path/file2.bin") + + +def test_ssl_verification_for_bucket_download_fails(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + ) + + with pytest.raises(requests.exceptions.SSLError) as execinfo: + _ = as_string(bucket.download("some/other/path/file2.bin")) + assert "CERTIFICATE_VERIFY_FAILED" in str(execinfo) + + +def test_ssl_verification_for_bucket_download_can_be_bypassed(httpsserver): + response = "Client should not be able to retrieve this!" + httpsserver.serve_content(response, 200) + bucket = Bucket( + name="foo", + service=httpsserver.url, + username="user", + password="pw", + verify=False, + ) + + with does_not_raise(requests.exceptions.SSLError): + _ = as_string(bucket.download("some/other/path/file2.bin"))