From 108f704b01c1edd88ea44ced001245175f5c49e7 Mon Sep 17 00:00:00 2001 From: SerRichard Date: Tue, 28 May 2024 11:46:56 +0200 Subject: [PATCH 1/3] adding the option for auth --- docs/guide/auth.md | 21 +++++++++++++++++++- openeo_fastapi/api/app.py | 19 +++++++++++------- openeo_fastapi/client/auth.py | 28 ++++++++++++++++++--------- openeo_fastapi/client/core.py | 36 +++++++++++++++-------------------- pyproject.toml | 2 +- tests/api/test_api.py | 26 ++++++++++++++++++++++++- 6 files changed, 92 insertions(+), 40 deletions(-) diff --git a/docs/guide/auth.md b/docs/guide/auth.md index 65e1cbb..13486fc 100644 --- a/docs/guide/auth.md +++ b/docs/guide/auth.md @@ -1 +1,20 @@ -## To be defined \ No newline at end of file +## Overwriting the authentication. + +The authentication function for this repo has been set using the predefined validator function in the Authenticator class. The following example will demonstrate how to provide your own auth function for a given endpoint. + +## How to overwrite the auth. + +You can either inherit the authenticator class and overwrite the validate function, or provide an entirely new function. In either case, it's worth noting that the return value for any provided authentication function currently needs to be of type User. If you want to additionally change the return type you may have to overwrite the endpoint entirely. + + client = OpenEOCore( + ... + ) + + api = OpenEOApi(client=client, app=FastAPI()) + + def cool_new_auth(): + return User(user_id=specific_uuid, oidc_sub="the-only-user") + + core_api.override_authentication(cool_new_auth) + +Now any endpoints that originally used the Authenticator.validate function, will now use cool_new_auth instead. diff --git a/openeo_fastapi/api/app.py b/openeo_fastapi/api/app.py index cc43bad..6257e1a 100644 --- a/openeo_fastapi/api/app.py +++ b/openeo_fastapi/api/app.py @@ -1,13 +1,15 @@ """OpenEO Api class for preparing the FastApi object from the client that is provided by the user. """ import attr -from fastapi import APIRouter, HTTPException, Response +from fastapi import APIRouter, Depends, HTTPException, Response from starlette.responses import JSONResponse from openeo_fastapi.api import models +from openeo_fastapi.client.auth import Authenticator HIDDEN_PATHS = ["/openapi.json", "/docs", "/docs/oauth2-redirect", "/redoc"] + @attr.define class OpenEOApi: """Factory for creating FastApi applications conformant to the OpenEO Api specification.""" @@ -17,9 +19,11 @@ class OpenEOApi: router: APIRouter = attr.ib(default=attr.Factory(APIRouter)) response_class: type[Response] = attr.ib(default=JSONResponse) + def override_authentication(self, func): + self.app.dependency_overrides[Authenticator.validate] = func + def register_well_known(self): - """Register well known endpoint (GET /.well-known/openeo). - """ + """Register well known endpoint (GET /.well-known/openeo).""" self.router.add_api_route( name=".well-known", path="/.well-known/openeo", @@ -410,7 +414,7 @@ def register_delete_file(self): def register_core(self): """ - Add application logic to the API layer. + Add application logic to the API layer. """ self.register_get_conformance() self.register_get_health() @@ -446,15 +450,16 @@ def register_core(self): def http_exception_handler(self, request, exception): """ - Register exception handler to turn python exceptions into expected OpenEO error output. + Register exception handler to turn python exceptions into expected OpenEO error output. """ - + exception_headers = { "allow_origin": "*", "allow_credentials": "true", "allow_methods": "*", } from fastapi.encoders import jsonable_encoder + return JSONResponse( headers=exception_headers, status_code=exception.status_code, @@ -463,7 +468,7 @@ def http_exception_handler(self, request, exception): def __attrs_post_init__(self): """ - Post-init hook responsible for setting up the application upon instantiation of the class. + Post-init hook responsible for setting up the application upon instantiation of the class. """ # Register core endpoints self.register_core() diff --git a/openeo_fastapi/client/auth.py b/openeo_fastapi/client/auth.py index b968535..2326a58 100644 --- a/openeo_fastapi/client/auth.py +++ b/openeo_fastapi/client/auth.py @@ -13,7 +13,7 @@ from enum import Enum import requests -from fastapi import Header, HTTPException +from fastapi import Header, HTTPException, Request from pydantic import BaseModel, ValidationError, validator from openeo_fastapi.api.types import Error @@ -33,7 +33,8 @@ class User(BaseModel): created_at: datetime.datetime = datetime.datetime.utcnow() class Config: - """Pydantic model class config.""" + """Pydantic model class config.""" + orm_mode = True arbitrary_types_allowed = True extra = "ignore" @@ -47,8 +48,8 @@ def get_orm(cls): # TODO Might make more sense to merge with IssueHandler class. # TODO The validate function needs to be easier to overwrite and inject into the OpenEO Core client. class Authenticator(ABC): - """Basic class to hold the validation call to be used by the api endpoints requiring authentication. - """ + """Basic class to hold the validation call to be used by the api endpoints requiring authentication.""" + # Authenticator validate method needs to know what decisions to make based on user info response from the issuer handler. # This will be different for different backends, so just put it as ABC for now. We might be able to define this if we want # to specify an auth config when initialising the backend. @@ -82,7 +83,7 @@ def validate(authorization: str = Header()): user = User(user_id=uuid.uuid4(), oidc_sub=user_info["sub"]) create(create_object=user) - + print("user ", user) return user @@ -123,6 +124,7 @@ def check_token(cls, v, values, **kwargs): @classmethod def from_token(cls, token: str): """Takes the openeo format token, splits it into the component parts, and returns an Auth token.""" + print("TOKEN: ", token) return cls( **dict(zip(["bearer", "method", "provider", "token"], token.split("/"))) ) @@ -186,7 +188,10 @@ def _validate_oidc_token(self, token: str): if issuer_oidc_config.status_code != 200: raise HTTPException( status_code=500, - detail=Error(code="InvalidIssuerConfig", message=f"The issuer config is not available. Tokens cannot be validated currently. Try again later."), + detail=Error( + code="InvalidIssuerConfig", + message=f"The issuer config is not available. Tokens cannot be validated currently. Try again later.", + ), ) userinfo_url = issuer_oidc_config.json()[OIDC_USERINFO] @@ -195,7 +200,9 @@ def _validate_oidc_token(self, token: str): if resp.status_code != 200: raise HTTPException( status_code=500, - detail=Error(code="TokenInvalid", message=f"The provided token is not valid."), + detail=Error( + code="TokenInvalid", message=f"The provided token is not valid." + ), ) return resp.json() @@ -217,8 +224,11 @@ def validate_token(self, token: str): if parsed_token.method.value == AuthMethod.OIDC.value: return self._validate_oidc_token(parsed_token.token) - + raise HTTPException( status_code=500, - detail=Error(code="TokenCantBeValidated", message=f"The provided token cannot be validated."), + detail=Error( + code="TokenCantBeValidated", + message=f"The provided token cannot be validated.", + ), ) diff --git a/openeo_fastapi/client/core.py b/openeo_fastapi/client/core.py index 8bf599a..255abd6 100644 --- a/openeo_fastapi/client/core.py +++ b/openeo_fastapi/client/core.py @@ -3,26 +3,23 @@ Classes: - OpenEOCore: Framework for defining the application logic that will passed onto the OpenEO Api. """ +import logging from collections import namedtuple -from typing import Optional +from typing import Any, Optional from urllib.parse import urlunparse from attrs import define, field -from fastapi import Depends, HTTPException, Response +from fastapi import Depends, HTTPException, Request, Response from openeo_fastapi.api.models import ( Capabilities, ConformanceGetResponse, FileFormatsGetResponse, MeGetResponse, + UdfRuntimesGetResponse, WellKnownOpeneoGetResponse, - UdfRuntimesGetResponse -) -from openeo_fastapi.api.types import ( - Error, - STACConformanceClasses, - Version, ) +from openeo_fastapi.api.types import Error, STACConformanceClasses, Version from openeo_fastapi.client.auth import Authenticator, User from openeo_fastapi.client.collections import CollectionRegister from openeo_fastapi.client.files import FilesRegister @@ -50,10 +47,9 @@ class OpenEOCore: processes: Optional[ProcessRegister] = None def __attrs_post_init__(self): - """Post init hook to set the client registers, if none where provided by the user set to the defaults! - """ + """Post init hook to set the client registers, if none where provided by the user set to the defaults!""" self.settings = AppSettings() - + self.collections = self.collections or CollectionRegister(self.settings) self.files = self.files or FilesRegister(self.settings, self.links) self.jobs = self.jobs or JobsRegister(self.settings, self.links) @@ -61,7 +57,7 @@ def __attrs_post_init__(self): def _combine_endpoints(self): """For the various registers that hold endpoint functions, concat those endpoints to register in get_capabilities. - + Returns: List: A list of all the endpoints that will be supported by this api deployment. """ @@ -100,10 +96,10 @@ def get_conformance(self) -> ConformanceGetResponse: return ConformanceGetResponse( conformsTo=[ STACConformanceClasses.CORE.value, - STACConformanceClasses.COLLECTIONS.value + STACConformanceClasses.COLLECTIONS.value, ] ) - + def get_file_formats(self) -> FileFormatsGetResponse: """Get the supported file formats for processing input and output. @@ -131,13 +127,13 @@ def get_user_info( Returns: MeGetResponse: The user information for the validated user. """ - return MeGetResponse(user_id=user.user_id.__str__()) - + return MeGetResponse(user_id=user.user_id) + def get_well_known(self) -> WellKnownOpeneoGetResponse: """Get the supported file formats for processing input and output. Returns: - WellKnownOpeneoGetResponse: The api/s which are exposed at this server. + WellKnownOpeneoGetResponse: The api/s which are exposed at this server. """ prefix = "https" if self.settings.API_TLS else "http" @@ -171,13 +167,11 @@ def get_udf_runtimes(self) -> UdfRuntimesGetResponse: Raises: HTTPException: Raises an exception with relevant status code and descriptive message of failure. - + Returns: UdfRuntimesGetResponse: The metadata for the requested BatchJob. """ raise HTTPException( status_code=501, - detail=Error( - code="FeatureUnsupported", message="Feature not supported." - ), + detail=Error(code="FeatureUnsupported", message="Feature not supported."), ) diff --git a/pyproject.toml b/pyproject.toml index 65eadd9..05f43a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openeo-fastapi" -version = "2024.4.2" +version = "2024.5.1" description = "FastApi implementation conforming to the OpenEO Api specification." authors = ["Sean Hoyal "] readme = "README.md" diff --git a/tests/api/test_api.py b/tests/api/test_api.py index 6f607bc..714f274 100644 --- a/tests/api/test_api.py +++ b/tests/api/test_api.py @@ -1,3 +1,4 @@ +import uuid from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Response @@ -95,7 +96,6 @@ def test_get_conformance(core_api, app_settings): def test_get_file_formats(core_api, app_settings): """Test the /conformance endpoint as intended.""" - test_app = TestClient(core_api.app) response = test_app.get(f"/{app_settings.OPENEO_VERSION}/file_formats") @@ -291,3 +291,27 @@ def get_file_headers( ) assert response.status_code == 200 + + +def test_overwrite_authenticator_validate( + mocked_oidc_config, mocked_oidc_userinfo, core_api, app_settings +): + """Test the user info is available.""" + + test_app = TestClient(core_api.app) + + specific_uuid = uuid.uuid4() + + def my_new_cool_auth(): + return User(user_id=specific_uuid, oidc_sub="the-real-user") + + core_api.override_authentication(my_new_cool_auth) + + response = test_app.get( + f"/{app_settings.OPENEO_VERSION}/me", + headers={"Authorization": "Bearer /oidc/egi/not-real"}, + ) + + assert response.status_code == 200 + assert "user_id" in response.json() + assert response.json()["user_id"] == str(specific_uuid) From 679321f03fbd288beb191e56f7b4f249275521e9 Mon Sep 17 00:00:00 2001 From: SerRichard Date: Tue, 28 May 2024 11:48:23 +0200 Subject: [PATCH 2/3] remove some unused imports and prints --- openeo_fastapi/client/auth.py | 4 +--- openeo_fastapi/client/core.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openeo_fastapi/client/auth.py b/openeo_fastapi/client/auth.py index 2326a58..541123b 100644 --- a/openeo_fastapi/client/auth.py +++ b/openeo_fastapi/client/auth.py @@ -13,7 +13,7 @@ from enum import Enum import requests -from fastapi import Header, HTTPException, Request +from fastapi import Header, HTTPException from pydantic import BaseModel, ValidationError, validator from openeo_fastapi.api.types import Error @@ -83,7 +83,6 @@ def validate(authorization: str = Header()): user = User(user_id=uuid.uuid4(), oidc_sub=user_info["sub"]) create(create_object=user) - print("user ", user) return user @@ -124,7 +123,6 @@ def check_token(cls, v, values, **kwargs): @classmethod def from_token(cls, token: str): """Takes the openeo format token, splits it into the component parts, and returns an Auth token.""" - print("TOKEN: ", token) return cls( **dict(zip(["bearer", "method", "provider", "token"], token.split("/"))) ) diff --git a/openeo_fastapi/client/core.py b/openeo_fastapi/client/core.py index 255abd6..6012aaf 100644 --- a/openeo_fastapi/client/core.py +++ b/openeo_fastapi/client/core.py @@ -3,13 +3,12 @@ Classes: - OpenEOCore: Framework for defining the application logic that will passed onto the OpenEO Api. """ -import logging from collections import namedtuple from typing import Any, Optional from urllib.parse import urlunparse from attrs import define, field -from fastapi import Depends, HTTPException, Request, Response +from fastapi import Depends, HTTPException, Response from openeo_fastapi.api.models import ( Capabilities, From 1ba6c9840b617d15b337fb9c964b6b0b48173249 Mon Sep 17 00:00:00 2001 From: SerRichard Date: Tue, 28 May 2024 11:48:58 +0200 Subject: [PATCH 3/3] remove any --- openeo_fastapi/client/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo_fastapi/client/core.py b/openeo_fastapi/client/core.py index 6012aaf..2afe75e 100644 --- a/openeo_fastapi/client/core.py +++ b/openeo_fastapi/client/core.py @@ -4,7 +4,7 @@ - OpenEOCore: Framework for defining the application logic that will passed onto the OpenEO Api. """ from collections import namedtuple -from typing import Any, Optional +from typing import Optional from urllib.parse import urlunparse from attrs import define, field