Skip to content
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
30 changes: 29 additions & 1 deletion app/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,35 @@ async def websocket_authenticate(websocket: WebSocket) -> str | None:
return None


async def exchange_token_for_provider(
async def exchange_token(user_token: str, url: str) -> str:
"""
Retrieve the exchanged token for accessing an external backend. This is done by exchanging the
user's token for a platform-specific token using the configured token provider.

:param url: The URL of the backend for which to exchange the token. This URL should be
configured in the BACKEND_CONFIG environment variable.
:return: The bearer token as a string.
"""

provider = settings.backend_auth_config[url].token_provider
token_prefix = settings.backend_auth_config[url].token_prefix

if not provider or not token_prefix:
raise ValueError(
f"Backend '{url}' must define 'token_provider' and 'token_prefix'"
)

platform_token = await _exchange_token_for_provider(
initial_token=user_token, provider=provider
)
return (
f"{token_prefix}/{platform_token['access_token']}"
if token_prefix
else platform_token["access_token"]
)


async def _exchange_token_for_provider(
initial_token: str, provider: str
) -> Dict[str, Any]:
"""
Expand Down
6 changes: 3 additions & 3 deletions app/config/openeo/settings.py → app/config/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from pydantic import BaseModel


class OpenEOAuthMethod(str, Enum):
class AuthMethod(str, Enum):
CLIENT_CREDENTIALS = "CLIENT_CREDENTIALS"
USER_CREDENTIALS = "USER_CREDENTIALS"


class OpenEOBackendConfig(BaseModel):
auth_method: OpenEOAuthMethod = OpenEOAuthMethod.USER_CREDENTIALS
class BackendAuthConfig(BaseModel):
auth_method: AuthMethod = AuthMethod.USER_CREDENTIALS
client_credentials: Optional[str] = None
token_provider: Optional[str] = None
token_prefix: Optional[str] = None
27 changes: 13 additions & 14 deletions app/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict

from app.config.openeo.settings import OpenEOAuthMethod, OpenEOBackendConfig
from app.config.schemas import AuthMethod, BackendAuthConfig


class Settings(BaseSettings):
Expand Down Expand Up @@ -40,29 +40,28 @@ class Settings(BaseSettings):
default="", json_schema_extra={"env": "KEYCLOAK_CLIENT_SECRET"}
)

# openEO Settings
openeo_backends: str | None = Field(
default="", json_schema_extra={"env": "OPENEO_BACKENDS"}
# Backend auth configuration
backends: str | None = Field(
default="", json_schema_extra={"env": "BACKENDS"}
)
backend_auth_config: Dict[str, BackendAuthConfig] = Field(default_factory=dict)

openeo_backend_config: Dict[str, OpenEOBackendConfig] = Field(default_factory=dict)

def load_openeo_backends_from_env(self):
def load_backends_auth_config(self):
"""
Populate self.backends from BACKENDS_JSON if provided, otherwise keep defaults.
BACKENDS_JSON should be a JSON object keyed by hostname with BackendConfig-like values.
"""
required_fields = []
if self.openeo_backends:
if self.backends:

try:
raw = json.loads(self.openeo_backends)
raw = json.loads(self.backends)
for host, cfg in raw.items():
backend = OpenEOBackendConfig(**cfg)
backend = BackendAuthConfig(**cfg)

if backend.auth_method == OpenEOAuthMethod.CLIENT_CREDENTIALS:
if backend.auth_method == AuthMethod.CLIENT_CREDENTIALS:
required_fields = ["client_credentials"]
elif backend.auth_method == OpenEOAuthMethod.USER_CREDENTIALS:
elif backend.auth_method == AuthMethod.USER_CREDENTIALS:
required_fields = ["token_provider"]

for field in required_fields:
Expand All @@ -71,11 +70,11 @@ def load_openeo_backends_from_env(self):
f"Backend '{host}' must define '{field}' when "
f"OPENEO_AUTH_METHOD={backend.auth_method}"
)
self.openeo_backend_config[host] = OpenEOBackendConfig(**cfg)
self.backend_auth_config[host] = BackendAuthConfig(**cfg)
except Exception:
# Fall back or raise as appropriate
raise


settings = Settings()
settings.load_openeo_backends_from_env()
settings.load_backends_auth_config()
43 changes: 11 additions & 32 deletions app/platforms/implementations/openeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from loguru import logger
from stac_pydantic import Collection

from app.auth import exchange_token_for_provider
from app.config.settings import OpenEOAuthMethod, settings
from app.auth import exchange_token
from app.config.schemas import AuthMethod
from app.config.settings import settings
from app.platforms.base import BaseProcessingPlatform
from app.platforms.dispatcher import register_platform
from app.schemas.enum import OutputFormatEnum, ProcessingStatusEnum, ProcessTypeEnum
Expand Down Expand Up @@ -58,28 +59,6 @@ def _connection_expired(self, connection: openeo.Connection) -> bool:
logger.warning("No JWT bearer token found in connection.")
return True

async def _get_bearer_token(self, user_token: str, url: str) -> str:
"""
Retrieve the bearer token for the OpenEO backend. This is done by exchanging the user's
token for a platform-specific token using the configured token provider.

:param url: The URL of the OpenEO backend.
:return: The bearer token as a string.
"""

provider = settings.openeo_backend_config[url].token_provider
token_prefix = settings.openeo_backend_config[url].token_prefix

if not provider or not token_prefix:
raise ValueError(
f"Backend '{url}' must define 'token_provider' and 'token_prefix'"
)

platform_token = await exchange_token_for_provider(
initial_token=user_token, provider=provider
)
return f"{token_prefix}/{platform_token['access_token']}"

async def _authenticate_user(
self, user_token: str, url: str, connection: openeo.Connection
) -> openeo.Connection:
Expand All @@ -88,19 +67,19 @@ async def _authenticate_user(
This method can be used to set the user's token for the connection.
"""

if url not in settings.openeo_backend_config:
if url not in settings.backend_auth_config:
raise ValueError(f"No OpenEO backend configuration found for URL: {url}")

if (
settings.openeo_backend_config[url].auth_method
== OpenEOAuthMethod.USER_CREDENTIALS
settings.backend_auth_config[url].auth_method
== AuthMethod.USER_CREDENTIALS
):
logger.debug("Using user credentials for OpenEO connection authentication")
bearer_token = await self._get_bearer_token(user_token, url)
bearer_token = await exchange_token(user_token=user_token, url=url)
connection.authenticate_bearer_token(bearer_token=bearer_token)
elif (
settings.openeo_backend_config[url].auth_method
== OpenEOAuthMethod.CLIENT_CREDENTIALS
settings.backend_auth_config[url].auth_method
== AuthMethod.CLIENT_CREDENTIALS
):
logger.debug(
"Using client credentials for OpenEO connection authentication"
Expand All @@ -115,7 +94,7 @@ async def _authenticate_user(
else:
raise ValueError(
"Unsupported OpenEO authentication method: "
f"{settings.openeo_backend_config[url].auth_method}"
f"{settings.backend_auth_config[url].auth_method}"
)

return connection
Expand Down Expand Up @@ -145,7 +124,7 @@ def _get_client_credentials(self, url: str) -> tuple[str, str, str]:
:param url: The URL of the OpenEO backend.
:return: A tuple containing provider ID, client ID, and client secret.
"""
credentials_str = settings.openeo_backend_config[url].client_credentials
credentials_str = settings.backend_auth_config[url].client_credentials

if not credentials_str:
raise ValueError(
Expand Down
Loading