Skip to content

Commit 4b6e38f

Browse files
committed
Issue #264/#187 add default_backend.auto_authenticate config
allow to use auto-auth fature only when using default_backend
1 parent 2d8e7fd commit 4b6e38f

File tree

5 files changed

+171
-61
lines changed

5 files changed

+171
-61
lines changed

docs/auth.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ add these configuration options to the :ref:`desired configuration file <configu
578578

579579
[Connection]
580580
default_backend = openeo.cloud
581-
auto_authenticate = oidc
581+
default_backend.auto_authenticate = oidc
582582

583583
Getting an authenticated connection is now as simple as::
584584

docs/configuration.rst

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,33 @@ The following configuration locations are probed (in this order) for an existing
6363
Configuration options
6464
----------------------
6565

66-
TODO
66+
.. list-table::
67+
:widths: 10 10 40
68+
:header-rows: 1
69+
70+
* - Config Section
71+
- Config
72+
- Description and possible values
73+
* - ``General``
74+
- ``verbose``
75+
- Verbosity mode when important config values are used:
76+
+ ``print``: always ``print()`` info
77+
+ ``auto`` (default): only ``print()`` when in an interactive context
78+
+ ``off``: don't print info
79+
* - ``Connection``
80+
- ``default_backend``
81+
- Default back-end to connect to when :py:func:`openeo.connect()`
82+
is used without explicit back-end URL.
83+
Also see :ref:`default_url_and_auto_auth`
84+
* - ``Connection``
85+
- ``default_backend.auto_authenticate``
86+
- Automatically authenticate in :py:func:`openeo.connect()` when using the ``default_backend`` config. Allowed values:
87+
+ ``basic`` for basic authentication
88+
+ ``oidc`` for OpenID Connect authentication
89+
+ ``off`` (default) for no authentication
90+
Also see :ref:`default_url_and_auto_auth`
91+
* - ``Connection``
92+
- ``auto_authenticate``
93+
- Automatically authenticate in :py:func:`openeo.connect()`.
94+
Allowed values: see ``default_backend.auto_authenticate``.
95+
Also see :ref:`default_url_and_auto_auth`

openeo/rest/connection.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,11 @@ def _config_log(message):
11771177
if default_backend:
11781178
url = default_backend
11791179
_config_log(f"Using default back-end URL {url!r} (from config)")
1180+
default_backend_auto_auth = get_config_option("connection.default_backend.auto_authenticate")
1181+
if default_backend_auto_auth and default_backend_auto_auth.lower() in {"basic", "oidc"}:
1182+
auth_type = default_backend_auto_auth.lower()
1183+
_config_log(f"Doing auto-authentication {auth_type!r} (from config)")
1184+
11801185
if auth_type is None:
11811186
auto_authenticate = get_config_option("connection.auto_authenticate")
11821187
if auto_authenticate and auto_authenticate.lower() in {"basic", "oidc"}:

tests/rest/auth/test_oidc.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,11 +455,14 @@ def _build_token_response(self, sub="123", name="john", include_id_token=True) -
455455

456456

457457
@contextlib.contextmanager
458-
def assert_device_code_poll_sleep():
459-
"""Fake sleeping, but check it was called with poll interval."""
458+
def assert_device_code_poll_sleep(expect_called=True):
459+
"""Fake sleeping, but check it was called with poll interval (or not)."""
460460
with mock.patch("time.sleep") as sleep:
461461
yield
462-
sleep.assert_called_with(DEVICE_CODE_POLL_INTERVAL)
462+
if expect_called:
463+
sleep.assert_called_with(DEVICE_CODE_POLL_INTERVAL)
464+
else:
465+
sleep.assert_not_called()
463466

464467

465468
@pytest.mark.slow

tests/rest/test_connection.py

Lines changed: 129 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,66 +1840,107 @@ def custom_client_config(tmp_path):
18401840
("print", True),
18411841
("off", False),
18421842
])
1843-
def test_connect_default_backend_from_config(requests_mock, custom_client_config, caplog, capsys, verbose, on_stdout):
1843+
@pytest.mark.parametrize("use_default", [False, True])
1844+
def test_connect_default_backend_from_config(
1845+
requests_mock, custom_client_config, caplog, capsys,
1846+
verbose, on_stdout, use_default
1847+
):
18441848
caplog.set_level(logging.INFO)
1845-
url = f"openeo{random.randint(0, 1000)}.test"
1849+
default = f"https://openeo{random.randint(0, 1000)}.test"
1850+
other = f"https://other{random.randint(0, 1000)}.test"
18461851
custom_client_config.write_text(textwrap.dedent(f"""
18471852
[General]
18481853
verbose = {verbose}
18491854
[Connection]
1850-
default_backend = {url}
1855+
default_backend = {default}
18511856
"""))
1852-
requests_mock.get(f"https://{url}", json={"api_version": "1.0.0"})
1853-
con = connect()
1854-
assert con.root_url == f"https://{url}"
1857+
requests_mock.get(default, json={"api_version": "1.0.0"})
1858+
requests_mock.get(other, json={"api_version": "1.0.0"})
18551859

1856-
expected_log = f"Using default back-end URL {url!r} (from config)"
1857-
assert expected_log in caplog.text
1858-
assert (expected_log in capsys.readouterr().out) == on_stdout
1860+
expected_log = f"Using default back-end URL {default!r} (from config)"
1861+
if use_default:
1862+
# Without arguments: use default
1863+
con = connect()
1864+
assert con.root_url == default
1865+
assert expected_log in caplog.text
1866+
assert (expected_log in capsys.readouterr().out) == on_stdout
1867+
else:
1868+
# With url argument: still works, and no config related output
1869+
con = connect(other)
1870+
assert con.root_url == other
1871+
assert expected_log not in caplog.text
1872+
assert expected_log not in capsys.readouterr().out
18591873

18601874

18611875
@pytest.mark.parametrize(["verbose", "on_stdout"], [
18621876
("print", True),
18631877
("off", False),
18641878
])
1879+
@pytest.mark.parametrize(["auto_auth_config", "use_default", "authenticated"], [
1880+
("auto_authenticate", True, True),
1881+
("auto_authenticate", False, True),
1882+
("default_backend.auto_authenticate", True, True),
1883+
("default_backend.auto_authenticate", False, False),
1884+
])
18651885
def test_connect_auto_auth_from_config_basic(
1866-
requests_mock, custom_client_config, auth_config, caplog, capsys, verbose, on_stdout
1886+
requests_mock, custom_client_config, auth_config, caplog, capsys,
1887+
verbose, on_stdout, auto_auth_config, use_default, authenticated,
18671888
):
18681889
caplog.set_level(logging.INFO)
1869-
url = f"https://openeo{random.randint(0, 1000)}.test"
1890+
default = f"https://openeo{random.randint(0, 1000)}.test"
1891+
other = f"https://other{random.randint(0, 1000)}.test"
18701892
custom_client_config.write_text(textwrap.dedent(f"""
18711893
[General]
18721894
verbose = {verbose}
18731895
[Connection]
1874-
default_backend = {url}
1875-
auto_authenticate = basic
1896+
default_backend = {default}
1897+
{auto_auth_config} = basic
18761898
"""))
18771899
user, pwd = "john", "j0hn"
1878-
auth_config.set_basic_auth(backend=url, username=user, password=pwd)
1900+
for u, a in [(default, "Hell0!"), (other, "Wazz6!")]:
1901+
auth_config.set_basic_auth(backend=u, username=user, password=pwd)
1902+
requests_mock.get(u, json={"api_version": "1.0.0", "endpoints": BASIC_ENDPOINTS})
1903+
requests_mock.get(f"{u}/credentials/basic", text=_credentials_basic_handler(user, pwd, access_token=a))
18791904

1880-
requests_mock.get(url, json={"api_version": "1.0.0", "endpoints": BASIC_ENDPOINTS})
1881-
requests_mock.get(f"{url}/credentials/basic", text=_credentials_basic_handler(user, pwd, access_token="Hell0!"))
1882-
1883-
con = connect()
1884-
assert con.root_url == url
1885-
assert isinstance(con.auth, BearerAuth)
1886-
assert con.auth.bearer == "basic//Hell0!"
1905+
if use_default:
1906+
# Without arguments: use default
1907+
con = connect()
1908+
assert con.root_url == default
1909+
else:
1910+
# With url argument: still works (possibly with auto_auth too)
1911+
con = connect(other)
1912+
assert con.root_url == other
18871913

18881914
expected_log = f"Doing auto-authentication 'basic' (from config)"
1889-
assert expected_log in caplog.text
1890-
assert (expected_log in capsys.readouterr().out) == on_stdout
1915+
if authenticated:
1916+
assert isinstance(con.auth, BearerAuth)
1917+
assert con.auth.bearer == "basic//Hell0!" if use_default else "basic//Wazz6!"
1918+
assert expected_log in caplog.text
1919+
assert (expected_log in capsys.readouterr().out) == on_stdout
1920+
else:
1921+
assert isinstance(con.auth, NullAuth)
1922+
assert expected_log not in caplog.text
1923+
assert expected_log not in capsys.readouterr().out
18911924

18921925

18931926
@pytest.mark.parametrize(["verbose", "on_stdout"], [
18941927
("print", True),
18951928
("off", False),
18961929
])
1930+
@pytest.mark.parametrize(["auto_auth_config", "use_default", "authenticated"], [
1931+
("auto_authenticate", True, True),
1932+
("auto_authenticate", False, True),
1933+
("default_backend.auto_authenticate", True, True),
1934+
("default_backend.auto_authenticate", False, False),
1935+
])
18971936
def test_connect_auto_auth_from_config_oidc_refresh_token(
1898-
requests_mock, custom_client_config, auth_config, refresh_token_store, caplog, capsys, verbose, on_stdout
1937+
requests_mock, custom_client_config, auth_config, refresh_token_store, caplog, capsys,
1938+
verbose, on_stdout, auto_auth_config, use_default, authenticated,
18991939
):
19001940
"""Auto-authorize with client config, auth config and refresh tokens"""
19011941
caplog.set_level(logging.INFO)
1902-
api_url = f"https://openeo{random.randint(0, 1000)}.test"
1942+
default = f"https://openeo{random.randint(0, 1000)}.test"
1943+
other = f"https://other{random.randint(0, 1000)}.test"
19031944
client_id = "myclient"
19041945
refresh_token = "r3fr35h!"
19051946
issuer = "https://oidc.test"
@@ -1908,14 +1949,15 @@ def test_connect_auto_auth_from_config_oidc_refresh_token(
19081949
[General]
19091950
verbose = {verbose}
19101951
[Connection]
1911-
default_backend = {api_url}
1912-
auto_authenticate = oidc
1952+
default_backend = {default}
1953+
{auto_auth_config} = oidc
19131954
"""))
19141955

1915-
requests_mock.get(api_url, json={"api_version": "1.0.0"})
1916-
requests_mock.get(f"{api_url}/credentials/oidc", json={
1917-
"providers": [{"id": "oi", "issuer": issuer, "title": "example", "scopes": ["openid"]}]
1918-
})
1956+
for u in [default, other]:
1957+
requests_mock.get(u, json={"api_version": "1.0.0"})
1958+
requests_mock.get(f"{u}/credentials/oidc", json={
1959+
"providers": [{"id": "oi", "issuer": issuer, "title": "example", "scopes": ["openid"]}]
1960+
})
19191961
oidc_mock = OidcMock(
19201962
requests_mock=requests_mock,
19211963
expected_grant_type="refresh_token",
@@ -1924,43 +1966,63 @@ def test_connect_auto_auth_from_config_oidc_refresh_token(
19241966
expected_fields={"refresh_token": refresh_token}
19251967
)
19261968
refresh_token_store.set_refresh_token(issuer=issuer, client_id=client_id, refresh_token=refresh_token)
1927-
auth_config.set_oidc_client_config(backend=api_url, provider_id="oi", client_id=client_id)
1969+
auth_config.set_oidc_client_config(backend=default, provider_id="oi", client_id=client_id)
1970+
auth_config.set_oidc_client_config(backend=other, provider_id="oi", client_id=client_id)
19281971

19291972
# With all this set up, now the real work:
1930-
con = connect()
1931-
assert con.root_url == api_url
1932-
assert isinstance(con.auth, BearerAuth)
1933-
assert con.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"]
1973+
if use_default:
1974+
con = connect()
1975+
assert con.root_url == default
1976+
else:
1977+
con = connect(other)
1978+
assert con.root_url == other
19341979

19351980
expected_log = f"Doing auto-authentication 'oidc' (from config)"
1936-
assert expected_log in caplog.text
1937-
assert (expected_log in capsys.readouterr().out) == on_stdout
1981+
if authenticated:
1982+
assert isinstance(con.auth, BearerAuth)
1983+
assert con.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"]
1984+
assert expected_log in caplog.text
1985+
assert (expected_log in capsys.readouterr().out) == on_stdout
1986+
else:
1987+
assert isinstance(con.auth, NullAuth)
1988+
assert expected_log not in caplog.text
1989+
assert expected_log not in capsys.readouterr().out
19381990

19391991

1992+
@pytest.mark.parametrize(["auto_auth_config", "use_default", "authenticated"], [
1993+
("auto_authenticate", True, True),
1994+
("auto_authenticate", False, True),
1995+
("default_backend.auto_authenticate", True, True),
1996+
("default_backend.auto_authenticate", False, False),
1997+
])
19401998
def test_connect_auto_auth_from_config_oidc_device_code(
1941-
requests_mock, custom_client_config, auth_config
1999+
requests_mock, custom_client_config, auth_config, caplog,
2000+
auto_auth_config, use_default, authenticated,
19422001
):
19432002
"""Auto-authorize without auth config or refresh tokens"""
1944-
api_url = f"https://openeo{random.randint(0, 1000)}.test"
2003+
caplog.set_level(logging.INFO)
2004+
default = f"https://openeo{random.randint(0, 1000)}.test"
2005+
other = f"https://other{random.randint(0, 1000)}.test"
19452006
default_client_id = "dadefaultklient"
19462007
grant_types = ["urn:ietf:params:oauth:grant-type:device_code+pkce", "refresh_token"]
19472008
issuer = "https://auth.test"
19482009

19492010
custom_client_config.write_text(textwrap.dedent(f"""
19502011
[Connection]
1951-
default_backend = {api_url}
1952-
auto_authenticate = oidc
2012+
default_backend = {default}
2013+
{auto_auth_config} = oidc
19532014
"""))
19542015

1955-
requests_mock.get(api_url, json={"api_version": "1.0.0"})
1956-
requests_mock.get(f"{api_url}/credentials/oidc", json={
1957-
"providers": [
1958-
{
1959-
"id": "auth", "issuer": issuer, "title": "Auth", "scopes": ["openid"],
1960-
"default_clients": [{"id": default_client_id, "grant_types": grant_types}]
1961-
},
1962-
]
1963-
})
2016+
for u in [default, other]:
2017+
requests_mock.get(u, json={"api_version": "1.0.0"})
2018+
requests_mock.get(f"{u}/credentials/oidc", json={
2019+
"providers": [
2020+
{
2021+
"id": "auth", "issuer": issuer, "title": "Auth", "scopes": ["openid"],
2022+
"default_clients": [{"id": default_client_id, "grant_types": grant_types}]
2023+
},
2024+
]
2025+
})
19642026

19652027
expected_fields = {
19662028
"scope": "openid",
@@ -1979,11 +2041,22 @@ def test_connect_auto_auth_from_config_oidc_device_code(
19792041

19802042
# With all this set up, now the real work:
19812043
oidc_mock.state["device_code_callback_timeline"] = ["great success"]
1982-
with assert_device_code_poll_sleep():
1983-
con = connect()
1984-
assert con.root_url == api_url
1985-
assert isinstance(con.auth, BearerAuth)
1986-
assert con.auth.bearer == "oidc/auth/" + oidc_mock.state["access_token"]
2044+
with assert_device_code_poll_sleep(expect_called=authenticated):
2045+
if use_default:
2046+
con = connect()
2047+
assert con.root_url == default
2048+
else:
2049+
con = connect(other)
2050+
assert con.root_url == other
2051+
2052+
expected_log = f"Doing auto-authentication 'oidc' (from config)"
2053+
if authenticated:
2054+
assert isinstance(con.auth, BearerAuth)
2055+
assert con.auth.bearer == "oidc/auth/" + oidc_mock.state["access_token"]
2056+
assert expected_log in caplog.text
2057+
else:
2058+
assert isinstance(con.auth, NullAuth)
2059+
assert expected_log not in caplog.text
19872060

19882061

19892062
@pytest.mark.parametrize(["capabilities", "expected"], [

0 commit comments

Comments
 (0)