Skip to content

Commit

Permalink
adding the option for auth (#48)
Browse files Browse the repository at this point in the history
* adding the option for auth

* remove some unused imports and prints

* remove any
  • Loading branch information
SerRichard authored May 28, 2024
1 parent 8028e75 commit b334c77
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 37 deletions.
21 changes: 20 additions & 1 deletion docs/guide/auth.md
Original file line number Diff line number Diff line change
@@ -1 +1,20 @@
## To be defined
## 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.
19 changes: 12 additions & 7 deletions openeo_fastapi/api/app.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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",
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand Down
24 changes: 16 additions & 8 deletions openeo_fastapi/client/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -82,7 +83,6 @@ def validate(authorization: str = Header()):
user = User(user_id=uuid.uuid4(), oidc_sub=user_info["sub"])

create(create_object=user)

return user


Expand Down Expand Up @@ -186,7 +186,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]
Expand All @@ -195,7 +198,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()
Expand All @@ -217,8 +222,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.",
),
)
31 changes: 12 additions & 19 deletions openeo_fastapi/client/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,10 @@
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
Expand Down Expand Up @@ -50,18 +46,17 @@ 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)
self.processes = self.processes or ProcessRegister(self.links)

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.
"""
Expand Down Expand Up @@ -100,10 +95,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.
Expand Down Expand Up @@ -131,13 +126,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"

Expand Down Expand Up @@ -171,13 +166,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."),
)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <sean.hoyal@external.eodc.eu>"]
readme = "README.md"
Expand Down
26 changes: 25 additions & 1 deletion tests/api/test_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from typing import Optional

from fastapi import Depends, FastAPI, HTTPException, Response
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)

0 comments on commit b334c77

Please sign in to comment.