From 4b877485942a65488916112a4e7feacc66e57aa8 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 4 Nov 2025 15:35:32 +0100 Subject: [PATCH 1/3] Implement list_auth_providers --- openeo/rest/connection.py | 32 +++++++++++++++++++++ tests/rest/test_connection.py | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 5625ef479..509d1b5dd 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -9,6 +9,7 @@ import os import shlex import urllib.parse +import uuid import warnings from collections import OrderedDict from pathlib import Path, PurePosixPath @@ -211,6 +212,37 @@ def _get_refresh_token_store(self) -> RefreshTokenStore: self._refresh_token_store = RefreshTokenStore() return self._refresh_token_store + def list_auth_providers(self) -> list[dict]: + providers = [] + cap = self.capabilities() + + # Add OIDC providers + oidc_path = "/credentials/oidc" + if cap.supports_endpoint(oidc_path, method="GET"): + try: + data = self.get(oidc_path, expected_status=200).json() + if isinstance(data, dict): + for provider in data.get("providers", []): + provider["type"] = "oidc" + providers.append(provider) + except OpenEoApiError: + pass + + # Add Basic provider + basic_path = "/credentials/basic" + if cap.supports_endpoint(basic_path, method="GET"): + providers.append( + { + "id": uuid.uuid4().hex, + "issuer": self.build_url(basic_path), + "type": "basic", + "title": "Basic", + "description": "HTTP Basic authentication using username and password", + } + ) + + return providers + def authenticate_basic(self, username: Optional[str] = None, password: Optional[str] = None) -> Connection: """ Authenticate a user to the backend using basic username and password. diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 0fc0a3976..b9a6d620f 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -767,6 +767,59 @@ def test_create_connection_lazy_refresh_token_store(requests_mock): ) +def test_list_auth_providers(requests_mock, api_version): + requests_mock.get( + API_URL, + json={ + "api_version": api_version, + "endpoints": [ + {"methods": ["GET"], "path": "/credentials/basic"}, + {"methods": ["GET"], "path": "/credentials/oidc"}, + ], + }, + ) + requests_mock.get( + API_URL + "credentials/oidc", + json={ + "providers": [ + {"id": "p1", "issuer": "https://openeo.example", "title": "openEO", "scopes": ["openid"]}, + {"id": "p2", "issuer": "https://other.example", "title": "Other", "scopes": ["openid"]}, + ] + }, + ) + + conn = Connection(API_URL) + providers = conn.list_auth_providers() + assert len(providers) == 3 + + p1 = next(filter(lambda x: x["id"] == "p1", providers), None) + assert isinstance(p1, dict) + assert p1["type"] == "oidc" + assert p1["issuer"] == "https://openeo.example" + assert p1["title"] == "openEO" + + p2 = next(filter(lambda x: x["id"] == "p2", providers), None) + assert isinstance(p2, dict) + assert p2["type"] == "oidc" + assert p2["issuer"] == "https://other.example" + assert p2["title"] == "Other" + + basic = next(filter(lambda x: x["type"] == "basic", providers), None) + assert isinstance(basic, dict) + assert isinstance(basic["id"], str) + assert len(basic["id"]) > 0 + assert basic["issuer"] == API_URL + "credentials/basic" + assert basic["title"] == "Basic" + + +def test_list_auth_providers_empty(requests_mock, api_version): + requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": []}) + + conn = Connection(API_URL) + providers = conn.list_auth_providers() + assert len(providers) == 0 + + def test_authenticate_basic_no_support(requests_mock, api_version): requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": []}) From e77340e6c3b362491c2a47c76f8f7f13e8236b02 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 4 Nov 2025 15:44:55 +0100 Subject: [PATCH 2/3] Align with Web Editor terminology --- openeo/rest/connection.py | 4 ++-- tests/rest/test_connection.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 509d1b5dd..e2e90a3e4 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -236,8 +236,8 @@ def list_auth_providers(self) -> list[dict]: "id": uuid.uuid4().hex, "issuer": self.build_url(basic_path), "type": "basic", - "title": "Basic", - "description": "HTTP Basic authentication using username and password", + "title": "Internal", + "description": "The HTTP Basic authentication method is mostly used for development and testing purposes.", } ) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index b9a6d620f..7f99e039b 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -809,7 +809,7 @@ def test_list_auth_providers(requests_mock, api_version): assert isinstance(basic["id"], str) assert len(basic["id"]) > 0 assert basic["issuer"] == API_URL + "credentials/basic" - assert basic["title"] == "Basic" + assert basic["title"] == "Internal" def test_list_auth_providers_empty(requests_mock, api_version): From 9874dd70d825cd82e80c74e09c9098d07f7346d3 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Fri, 7 Nov 2025 20:08:19 +0100 Subject: [PATCH 3/3] Code review --- openeo/rest/connection.py | 7 +++---- tests/rest/test_connection.py | 36 +++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index e2e90a3e4..ba6f71f9a 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -9,7 +9,6 @@ import os import shlex import urllib.parse -import uuid import warnings from collections import OrderedDict from pathlib import Path, PurePosixPath @@ -225,15 +224,15 @@ def list_auth_providers(self) -> list[dict]: for provider in data.get("providers", []): provider["type"] = "oidc" providers.append(provider) - except OpenEoApiError: - pass + except OpenEoApiError as e: + _log.warning(f"Unable to load the OpenID Connect provider list: {e.message}") # Add Basic provider basic_path = "/credentials/basic" if cap.supports_endpoint(basic_path, method="GET"): providers.append( { - "id": uuid.uuid4().hex, + "id": basic_path, "issuer": self.build_url(basic_path), "type": "basic", "title": "Internal", diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 7f99e039b..a6f885cbc 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -768,16 +768,7 @@ def test_create_connection_lazy_refresh_token_store(requests_mock): def test_list_auth_providers(requests_mock, api_version): - requests_mock.get( - API_URL, - json={ - "api_version": api_version, - "endpoints": [ - {"methods": ["GET"], "path": "/credentials/basic"}, - {"methods": ["GET"], "path": "/credentials/oidc"}, - ], - }, - ) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) requests_mock.get( API_URL + "credentials/oidc", json={ @@ -804,20 +795,37 @@ def test_list_auth_providers(requests_mock, api_version): assert p2["issuer"] == "https://other.example" assert p2["title"] == "Other" - basic = next(filter(lambda x: x["type"] == "basic", providers), None) + basic = next(filter(lambda x: x["id"] == "/credentials/basic", providers), None) assert isinstance(basic, dict) - assert isinstance(basic["id"], str) - assert len(basic["id"]) > 0 + assert basic["type"] == "basic" assert basic["issuer"] == API_URL + "credentials/basic" assert basic["title"] == "Internal" def test_list_auth_providers_empty(requests_mock, api_version): - requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": []}) + requests_mock.get( + API_URL, + json=build_capabilities(api_version=api_version, basic_auth=False, oidc_auth=False), + ) + + conn = Connection(API_URL) + providers = conn.list_auth_providers() + assert len(providers) == 0 + + +def test_list_auth_providers_invalid(requests_mock, api_version, caplog): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version, basic_auth=False)) + error_message = "Maintenance ongoing" + requests_mock.get( + API_URL + "credentials/oidc", + status_code=500, + json={"code": "Internal", "message": error_message}, + ) conn = Connection(API_URL) providers = conn.list_auth_providers() assert len(providers) == 0 + assert f"Unable to load the OpenID Connect provider list: {error_message}" in caplog.messages def test_authenticate_basic_no_support(requests_mock, api_version):