Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding the option for auth #48

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>"]
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)
Loading