Skip to content

Commit

Permalink
Issue #187/#196 openeo.connect support auto-authentication from config
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Mar 29, 2022
1 parent 770d88e commit cb741d4
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 23 deletions.
11 changes: 7 additions & 4 deletions openeo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from configparser import ConfigParser
from copy import deepcopy
from pathlib import Path
from typing import Union, Any, Sequence, Iterator
from typing import Union, Any, Sequence, Iterator, Optional

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -164,9 +164,12 @@ def load(cls) -> ClientConfig:
_global_config = None


def get_config() -> ClientConfig:
"""Get global :py:class:`ClientConfig` (lazily loaded)."""
def get_config(key: Optional[str] = None, default=None) -> Union[ClientConfig, str]:
"""Get a value from (or the whole) global :py:class:`ClientConfig` (lazily loaded)."""
global _global_config
if _global_config is None:
_global_config = ConfigLoader.load()
return _global_config
if key:
return _global_config.get(key, default=default)
else:
return _global_config
4 changes: 3 additions & 1 deletion openeo/rest/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1156,10 +1156,12 @@ def connect(
"""
if url is None:
# TODO: print info about which connection is used (and based on which config)
url = get_config().get("connection.default_backend")
url = get_config("connection.default_backend")
if not url:
raise OpenEoClientException("No back-end URL given or known to connect to.")
connection = Connection(url, session=session, default_timeout=default_timeout)

auth_type = auth_type or get_config("connection.auto_authenticate")
auth_type = auth_type.lower() if isinstance(auth_type, str) else auth_type
if auth_type in {None, 'null', 'none'}:
pass
Expand Down
137 changes: 119 additions & 18 deletions tests/rest/test_connection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import json
import logging
import os
import random
import re
import textwrap
import typing
import unittest.mock as mock
import zlib
Expand Down Expand Up @@ -432,15 +434,20 @@ def test_api_error_non_json(requests_mock):
assert exc.url is None


def _credentials_basic_handler(username, password, access_token="w3lc0m3"):
expected_auth = requests.auth._basic_auth_str(username=username, password=password)

def handler(request, context):
assert request.headers["Authorization"] == expected_auth
return json.dumps({"access_token": access_token})

return handler


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, "endpoints": BASIC_ENDPOINTS})

def text_callback(request, context):
assert request.headers["Authorization"] == requests.auth._basic_auth_str(username=user, password=pwd)
return '{"access_token":"w3lc0m3"}'

requests_mock.get(API_URL + 'credentials/basic', text=text_callback)
requests_mock.get(API_URL + 'credentials/basic', text=_credentials_basic_handler(user, pwd))

with mock.patch('openeo.rest.connection.AuthConfig') as AuthConfig:
# Don't create default AuthConfig when not necessary
Expand Down Expand Up @@ -500,12 +507,7 @@ def test_authenticate_basic_no_support(requests_mock, api_version):
def test_authenticate_basic(requests_mock, api_version):
user, pwd = "john262", "J0hndo3"
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)
return '{"access_token":"w3lc0m3"}'

requests_mock.get(API_URL + 'credentials/basic', text=text_callback)
requests_mock.get(API_URL + 'credentials/basic', text=_credentials_basic_handler(user, pwd))

conn = Connection(API_URL)
assert isinstance(conn.auth, NullAuth)
Expand All @@ -520,12 +522,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, "endpoints": BASIC_ENDPOINTS})

def text_callback(request, context):
assert request.headers["Authorization"] == requests.auth._basic_auth_str(username=user, password=pwd)
return '{"access_token":"w3lc0m3"}'

requests_mock.get(API_URL + 'credentials/basic', text=text_callback)
requests_mock.get(API_URL + 'credentials/basic', text=_credentials_basic_handler(user, pwd))
auth_config.set_basic_auth(backend=API_URL, username=user, password=pwd)

conn = Connection(API_URL)
Expand Down Expand Up @@ -1844,3 +1841,107 @@ def test_connect_default_backend_from_config(requests_mock, custom_client_config
requests_mock.get(f"https://{url}", json={"api_version": "1.0.0"})
con = connect()
assert con.root_url == f"https://{url}"


def test_connect_auto_auth_from_config_basic(requests_mock, custom_client_config, auth_config):
url = f"https://openeo{random.randint(0, 1000)}.test"
custom_client_config.write_text(textwrap.dedent(f"""
[Connection]
default_backend = {url}
auto_authenticate = basic
"""))
user, pwd = "john", "j0hn"
auth_config.set_basic_auth(backend=url, username=user, password=pwd)

requests_mock.get(url, json={"api_version": "1.0.0", "endpoints": BASIC_ENDPOINTS})
requests_mock.get(f"{url}/credentials/basic", text=_credentials_basic_handler(user, pwd, access_token="Hell0!"))

con = connect()
assert con.root_url == url
assert isinstance(con.auth, BearerAuth)
assert con.auth.bearer == "basic//Hell0!"


def test_connect_auto_auth_from_config_oidc_refresh_token(
requests_mock, custom_client_config, auth_config, refresh_token_store
):
"""Auto-authorize with client config, auth config and refresh tokens"""
api_url = f"https://openeo{random.randint(0, 1000)}.test"
client_id = "myclient"
refresh_token = "r3fr35h!"
issuer = "https://oidc.test"

custom_client_config.write_text(textwrap.dedent(f"""
[Connection]
default_backend = {api_url}
auto_authenticate = oidc
"""))

requests_mock.get(api_url, json={"api_version": "1.0.0"})
requests_mock.get(f"{api_url}/credentials/oidc", json={
"providers": [{"id": "oi", "issuer": issuer, "title": "example", "scopes": ["openid"]}]
})
oidc_mock = OidcMock(
requests_mock=requests_mock,
expected_grant_type="refresh_token",
expected_client_id=client_id,
oidc_discovery_url=f"{issuer}/.well-known/openid-configuration",
expected_fields={"refresh_token": refresh_token}
)
refresh_token_store.set_refresh_token(issuer=issuer, client_id=client_id, refresh_token=refresh_token)
auth_config.set_oidc_client_config(backend=api_url, provider_id="oi", client_id=client_id)

# With all this set up, now the real work:
con = connect()
assert con.root_url == api_url
assert isinstance(con.auth, BearerAuth)
assert con.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"]


def test_connect_auto_auth_from_config_oidc_device_code(
requests_mock, custom_client_config, auth_config
):
"""Auto-authorize without auth config or refresh tokens"""
api_url = f"https://openeo{random.randint(0, 1000)}.test"
default_client_id = "dadefaultklient"
grant_types = ["urn:ietf:params:oauth:grant-type:device_code+pkce", "refresh_token"]
issuer = "https://auth.test"

custom_client_config.write_text(textwrap.dedent(f"""
[Connection]
default_backend = {api_url}
auto_authenticate = oidc
"""))

requests_mock.get(api_url, json={"api_version": "1.0.0"})
requests_mock.get(f"{api_url}/credentials/oidc", json={
"providers": [
{
"id": "auth", "issuer": issuer, "title": "Auth", "scopes": ["openid"],
"default_clients": [{"id": default_client_id, "grant_types": grant_types}]
},
]
})

expected_fields = {
"scope": "openid",
"code_verifier": True,
"code_challenge": True
}
oidc_mock = OidcMock(
requests_mock=requests_mock,
expected_grant_type="urn:ietf:params:oauth:grant-type:device_code",
expected_client_id=default_client_id,
expected_fields=expected_fields,
scopes_supported=["openid"],
oidc_discovery_url=f"{issuer}/.well-known/openid-configuration",
)
assert auth_config.load() == {}

# With all this set up, now the real work:
oidc_mock.state["device_code_callback_timeline"] = ["great success"]
with assert_device_code_poll_sleep():
con = connect()
assert con.root_url == api_url
assert isinstance(con.auth, BearerAuth)
assert con.auth.bearer == "oidc/auth/" + oidc_mock.state["access_token"]

0 comments on commit cb741d4

Please sign in to comment.