diff --git a/README.md b/README.md index 28dd986..f31b059 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Included is a vscode dev container which is intended to be used as the developme ``` # From /openeo-fastapi + poetry config virtualenvs.path "" + poetry lock poetry install --all-extras diff --git a/openeo_fastapi/client/auth.py b/openeo_fastapi/client/auth.py index ed3b1bc..97a98d5 100644 --- a/openeo_fastapi/client/auth.py +++ b/openeo_fastapi/client/auth.py @@ -51,7 +51,7 @@ def check_provider(cls, v, values, **kwargs): raise ValidationError("Empty provider string.") return v - @validator("provider") + @validator("token", pre=True) def check_token(cls, v, values, **kwargs): if v == "": raise ValidationError("Empty token string.") diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..3b549d6 --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +create = true diff --git a/pyproject.toml b/pyproject.toml index 1b56c43..bd61861 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ attrs = "^23.1.0" httpx = "^0.24.1" pip = "^23.3.2" ipykernel = "^6.28.0" +requests = "^2.31.0" [tool.poetry.group.dev.dependencies] diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 7c01c80..8daf2bb 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -1,7 +1,18 @@ +from unittest.mock import patch + import pytest from pydantic import ValidationError -from openeo_fastapi.client import auth +from openeo_fastapi.client import auth, exceptions + +BASIC_TOKEN_EXAMPLE = "Bearer /basic/openeo/rubbish.not.a.token" +OIDC_TOKEN_EXAMPLE = "Bearer /oidc/issuer/rubbish.not.a.token" + +INVALID_TOKEN_EXAMPLE_1 = "bearer /basic/openeo/rubbish.not.a.token" +INVALID_TOKEN_EXAMPLE_2 = "Bearer /basicopeneorubbish.not.a.token" +INVALID_TOKEN_EXAMPLE_3 = "Bearer //openeo/rubbish.not.a.token" +INVALID_TOKEN_EXAMPLE_4 = "Bearer /basic//rubbish.not.a.token" +INVALID_TOKEN_EXAMPLE_5 = "Bearer /basic/openeo/" def test_auth_method(): @@ -24,32 +35,25 @@ def token_checks(token: auth.AuthToken, method: str, provider: str): assert token.method.value == method assert token.provider == provider - BASIC_TOKEN_EXAMPLE = "Bearer /basic/openeo/rubbish.not.a.token" basic_token = auth.AuthToken.from_token(BASIC_TOKEN_EXAMPLE) token_checks(basic_token, "basic", "openeo") - OIDC_TOKEN_EXAMPLE = "Bearer /oidc/issuer/rubbish.not.a.token" oidc_token = auth.AuthToken.from_token(OIDC_TOKEN_EXAMPLE) token_checks(oidc_token, "oidc", "issuer") # Check cases of invalid format raise a validation error. - INVALID_TOKEN_EXAMPLE_1 = "bearer /basic/openeo/rubbish.not.a.token" with pytest.raises(ValidationError): auth.AuthToken.from_token(INVALID_TOKEN_EXAMPLE_1) - INVALID_TOKEN_EXAMPLE_2 = "Bearer /basicopeneorubbish.not.a.token" with pytest.raises(ValidationError): auth.AuthToken.from_token(INVALID_TOKEN_EXAMPLE_2) - INVALID_TOKEN_EXAMPLE_3 = "Bearer //openeo/rubbish.not.a.token" with pytest.raises(ValidationError): auth.AuthToken.from_token(INVALID_TOKEN_EXAMPLE_3) - INVALID_TOKEN_EXAMPLE_4 = "Bearer /basic//rubbish.not.a.token" with pytest.raises(ValidationError): auth.AuthToken.from_token(INVALID_TOKEN_EXAMPLE_4) - INVALID_TOKEN_EXAMPLE_5 = "Bearer /basic/openeo/" with pytest.raises(ValidationError): auth.AuthToken.from_token(INVALID_TOKEN_EXAMPLE_5) @@ -63,23 +67,46 @@ def test_issuer_handler_init(): # Check trailing slash removal assert not test_issuer.issuer_url.endswith("/") + assert test_issuer.organisation == "mycloud" -def test_issuer_handler__validate_oidc_token(): - test_issuer = auth.IssuerHandler( - issuer_url="http://issuer.mycloud/", - organisation="mycloud", - roles=["admin", "user"], - ) +def test_issuer_handler__validate_oidc_token( + mocked_oidc_config, mocked_oidc_userinfo, mocked_issuer +): + info = mocked_issuer._validate_oidc_token(token=OIDC_TOKEN_EXAMPLE) + assert isinstance(info, auth.UserInfo) - assert True +def test_issuer_handler__validate_oidc_token_bad_config( + mocked_bad_oidc_config, mocked_oidc_userinfo, mocked_issuer +): + with pytest.raises(exceptions.InvalidIssuerConfig): + mocked_issuer._validate_oidc_token(token=OIDC_TOKEN_EXAMPLE) + + +def test_issuer_handler__validate_oidc_token_bad_userinfo( + mocked_oidc_config, mocked_bad_oidc_userinfo, mocked_issuer +): + with pytest.raises(exceptions.TokenInvalid): + mocked_issuer._validate_oidc_token(token=OIDC_TOKEN_EXAMPLE) -def test_issuer_handler_validate_token(): - test_issuer = auth.IssuerHandler( - issuer_url="http://issuer.mycloud/", - organisation="mycloud", - roles=["admin", "user"], - ) - assert True +def test_issuer_handler_validate_oidc_token( + mocked_oidc_config, mocked_oidc_userinfo, mocked_issuer +): + info = mocked_issuer.validate_token(token=OIDC_TOKEN_EXAMPLE) + assert isinstance(info, auth.UserInfo) + + +def test_issuer_handler_validate_basic_token( + mocked_oidc_config, mocked_oidc_userinfo, mocked_issuer +): + with pytest.raises(exceptions.TokenCantBeValidated): + mocked_issuer.validate_token(token=BASIC_TOKEN_EXAMPLE) + + +def test_issuer_handler_validate_broken_token( + mocked_oidc_config, mocked_oidc_userinfo, mocked_issuer +): + with pytest.raises(ValidationError): + mocked_issuer.validate_token(token=INVALID_TOKEN_EXAMPLE_1) diff --git a/tests/conftest.py b/tests/conftest.py index 757dab7..875cd25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,12 @@ +import json +from unittest.mock import patch + import pytest from fastapi import FastAPI +from requests import Response from openeo_fastapi.api.app import OpenEOApi -from openeo_fastapi.client import models +from openeo_fastapi.client import auth, models from openeo_fastapi.client.core import OpenEOCore @@ -40,3 +44,67 @@ def core_api(): api = OpenEOApi(client=client, app=FastAPI()) return api + + +@pytest.fixture() +def mocked_oidc_config(): + resp_content_bytes = json.dumps( + {"userinfo_endpoint": "http://nothere.test"} + ).encode("utf-8") + + mocked_response = Response() + mocked_response.status_code = 200 + mocked_response._content = resp_content_bytes + + with patch("openeo_fastapi.client.auth.IssuerHandler._get_issuer_config") as mock: + mock.return_value = mocked_response + yield mock + + +@pytest.fixture() +def mocked_oidc_userinfo(): + resp_content_bytes = json.dumps( + { + "eduperson_entitlement": [ + "entitlment", + ], + "sub": "someuser@testing.test", + } + ).encode("utf-8") + + mocked_response = Response() + mocked_response.status_code = 200 + mocked_response._content = resp_content_bytes + + with patch("openeo_fastapi.client.auth.IssuerHandler._get_user_info") as mock: + mock.return_value = mocked_response + yield mock + + +@pytest.fixture() +def mocked_bad_oidc_config(): + mocked_response = Response() + mocked_response.status_code = 404 + + with patch("openeo_fastapi.client.auth.IssuerHandler._get_issuer_config") as mock: + mock.return_value = mocked_response + yield mock + + +@pytest.fixture() +def mocked_bad_oidc_userinfo(): + mocked_response = Response() + mocked_response.status_code = 404 + + with patch("openeo_fastapi.client.auth.IssuerHandler._get_user_info") as mock: + mock.return_value = mocked_response + yield mock + + +@pytest.fixture() +def mocked_issuer(): + return auth.IssuerHandler( + issuer_url="http://issuer.mycloud/", + organisation="mycloud", + roles=["admin", "user"], + )