diff --git a/CHANGELOG.md b/CHANGELOG.md index e4712227b..dc3d5c916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Raise `ProcessGraphVisitException` from `ProcessGraphVisitor.resolve_from_node()` (instead of generic `ValueError`) - `DataCube.linear_scale_range` is now a shortcut for `DataCube.apply(lambda x:x.x.linear_scale_range( input_min, input_max, output_min, output_max))`. Instead of creating an invalid process graph that tries to invoke linear_scale_range on a datacube directly. +- Nicer error message when back-end does not support basic auth ([#247](https://github.com/Open-EO/openeo-python-client/issues/247)) ### Removed diff --git a/openeo/capabilities.py b/openeo/capabilities.py index d79d83fe0..c8b66db1d 100644 --- a/openeo/capabilities.py +++ b/openeo/capabilities.py @@ -2,6 +2,8 @@ from distutils.version import LooseVersion from typing import Union +# Is this base class (still) useful? + class Capabilities(ABC): """Represents capabilities of a connection / back end.""" diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 4526cfe1b..01109d86c 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -270,6 +270,8 @@ def authenticate_basic(self, username: str = None, password: str = None) -> 'Con :param username: User name :param password: User passphrase """ + if not self.capabilities().supports_endpoint("/credentials/basic", method="GET"): + raise OpenEoClientException("This openEO back-end does not support basic authentication.") if username is None: username, password = self._get_auth_config().get_basic_auth(backend=self._orig_url) if username is None: diff --git a/openeo/rest/rest_capabilities.py b/openeo/rest/rest_capabilities.py index 8cbc4bd5f..a507bd940 100644 --- a/openeo/rest/rest_capabilities.py +++ b/openeo/rest/rest_capabilities.py @@ -27,6 +27,12 @@ def has_features(self, method_name): # Field: endpoints > ... TODO pass + def supports_endpoint(self, path: str, method="GET"): + return any( + endpoint.get("path") == path and method.upper() in endpoint.get("methods", []) + for endpoint in self.capabilities.get("endpoints", []) + ) + def currency(self): """ Get default billing currency.""" return self.capabilities.get('billing', {}).get('currency') diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index b5b8f5a1c..7224508ba 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -19,8 +19,11 @@ from .auth.test_oidc import OidcMock, assert_device_code_poll_sleep, ABSENT from .. import load_json_resource + API_URL = "https://oeo.test/" +BASIC_ENDPOINTS = [{"path": "/credentials/basic", "methods": ["GET"]}] + # Trick to avoid linting/auto-formatting tools to complain about or fix unused imports of these pytest fixtures auth_config = auth_config refresh_token_store = refresh_token_store @@ -193,7 +196,7 @@ def test_connection_other_domain_auth_headers(requests_mock, api_version): def debug(request: requests.Request, context): return repr(("hello world", request.headers)) - requests_mock.get(API_URL, json={"api_version": api_version}) + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) requests_mock.get(API_URL + 'credentials/basic', json={"access_token": secret}) requests_mock.get("https://evilcorp.test/download/hello.txt", text=debug) @@ -300,7 +303,8 @@ def test_connection_repr(requests_mock): requests_mock.get("https://oeo.test/.well-known/openeo", status_code=200, json={ "versions": [{"api_version": "1.0.0", "url": "https://oeo.test/openeo/1.x/", "production": True}], }) - requests_mock.get("https://oeo.test/openeo/1.x/", status_code=200, json={"api_version": "1.0.0"}) + requests_mock.get("https://oeo.test/openeo/1.x/", status_code=200, + json={"api_version": "1.0.0", "endpoints": BASIC_ENDPOINTS}) requests_mock.get("https://oeo.test/openeo/1.x/credentials/basic", json={"access_token": "w3lc0m3"}) conn = connect("https://oeo.test/") @@ -360,7 +364,7 @@ def test_api_error_non_json(requests_mock): def test_create_connection_lazy_auth_config(requests_mock, api_version): user, pwd = "john262", "J0hndo3" - requests_mock.get(API_URL, json={"api_version": api_version}) + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) def text_callback(request, context): assert request.headers["Authorization"] == requests.auth._basic_auth_str(username=user, password=pwd) @@ -413,9 +417,19 @@ def test_create_connection_lazy_refresh_token_store(requests_mock): ) +def test_authenticate_basic_no_support(requests_mock, api_version): + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": []}) + + conn = Connection(API_URL) + assert isinstance(conn.auth, NullAuth) + with pytest.raises(OpenEoClientException, match="does not support basic auth"): + conn.authenticate_basic(username="john", password="j0hn") + assert isinstance(conn.auth, NullAuth) + + def test_authenticate_basic(requests_mock, api_version): user, pwd = "john262", "J0hndo3" - requests_mock.get(API_URL, json={"api_version": api_version}) + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) def text_callback(request, context): assert request.headers["Authorization"] == requests.auth._basic_auth_str(username=user, password=pwd) @@ -435,7 +449,7 @@ def text_callback(request, context): def test_authenticate_basic_from_config(requests_mock, api_version, auth_config): user, pwd = "john281", "J0hndo3" - requests_mock.get(API_URL, json={"api_version": api_version}) + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) def text_callback(request, context): assert request.headers["Authorization"] == requests.auth._basic_auth_str(username=user, password=pwd) @@ -1692,7 +1706,7 @@ def test_paginate_callback(requests_mock): lambda con: PGNode("add", x=3, y=5) ]) def test_as_curl(requests_mock, data_factory): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json={"api_version": "1.0.0", "endpoints": BASIC_ENDPOINTS}) requests_mock.get(API_URL + 'credentials/basic', json={"access_token": "s3cr6t"}) con = Connection(API_URL).authenticate_basic("john", "j0hn") diff --git a/tests/rest/test_imagecollectionclient.py b/tests/rest/test_imagecollectionclient.py index b36c3842e..0a5753e89 100644 --- a/tests/rest/test_imagecollectionclient.py +++ b/tests/rest/test_imagecollectionclient.py @@ -12,7 +12,10 @@ @pytest.fixture def session040(requests_mock): - requests_mock.get(API_URL + "/", json={"api_version": "0.4.0"}) + requests_mock.get(API_URL + "/", json={ + "api_version": "0.4.0", + "endpoints": [{"path": "/credentials/basic", "methods": ["GET"]}] + }) session = openeo.connect(API_URL) return session diff --git a/tests/rest/test_job.py b/tests/rest/test_job.py index e7973c87f..77200a97e 100644 --- a/tests/rest/test_job.py +++ b/tests/rest/test_job.py @@ -17,14 +17,20 @@ @pytest.fixture def session040(requests_mock): - requests_mock.get(API_URL + "/", json={"api_version": "0.4.0"}) + requests_mock.get(API_URL + "/", json={ + "api_version": "0.4.0", + "endpoints": [{"path": "/credentials/basic", "methods": ["GET"]}] + }) session = openeo.connect(API_URL) return session @pytest.fixture def con100(requests_mock): - requests_mock.get(API_URL + "/", json={"api_version": "1.0.0"}) + requests_mock.get(API_URL + "/", json={ + "api_version": "1.0.0", + "endpoints": [{"path": "/credentials/basic", "methods": ["GET"]}] + }) con = openeo.connect(API_URL) return con diff --git a/tests/test_usecase1.py b/tests/test_usecase1.py index 531fea987..69cbb7de0 100644 --- a/tests/test_usecase1.py +++ b/tests/test_usecase1.py @@ -9,6 +9,7 @@ @requests_mock.mock() class TestUsecase1(TestCase): + _capabilities = {"api_version": "0.4.0", "endpoints": [{"path": "/credentials/basic", "methods": ["GET"]}]} def setUp(self): # configuration phase: define username, endpoint, parameters? @@ -22,13 +23,13 @@ def setUp(self): self.output_file = "/tmp/test.gtiff" def test_user_login(self, m): - m.get("http://localhost:8000/api/", json={"api_version": "0.4.0"}) + m.get("http://localhost:8000/api/", json=self._capabilities) m.get("http://localhost:8000/api/credentials/basic", json={"access_token": "blabla"}) con = openeo.connect(self.endpoint).authenticate_basic(username=self.auth_id, password=self.auth_pwd) assert isinstance(con.auth, BearerAuth) def test_viewing_list_jobs(self, m): - m.get("http://localhost:8000/api/", json={"api_version": "0.4.0"}) + m.get("http://localhost:8000/api/", json=self._capabilities) m.get("http://localhost:8000/api/credentials/basic", json={"access_token": "blabla"}) job_info = {"job_id": "748df7caa8c84a7ff6e", "status": "running", "created": "2021-02-22T09:00:00Z"} m.get("http://localhost:8000/api/jobs", json={"jobs": [job_info]}) @@ -59,7 +60,7 @@ def test_viewing_processes(self, m): assert self.process_id in set(p["process_id"] for p in processes) def test_job_creation(self, m): - m.get("http://localhost:8000/api/", json={"api_version": "0.4.0"}) + m.get("http://localhost:8000/api/", json=self._capabilities) m.get("http://localhost:8000/api/credentials/basic", json={"access_token": "blabla"}) m.post("http://localhost:8000/api/jobs", status_code=201,headers={"OpenEO-Identifier": "748df7caa8c84a7ff6e"})