From 9f82d2e1843341c2e3173cdbe45ecae3710af32a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 001/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/config.py # packages/opal-server/opal_server/config.py # packages/requires.txt # Conflicts: # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/updater.py --- .../docker-compose-with-oauth-jwt-token.yml | 93 ++++++++++ ...docker-compose-with-oauth-opaque-token.yml | 83 +++++++++ .../opal-client/opal_client/callbacks/api.py | 4 +- packages/opal-client/opal_client/client.py | 37 ++-- .../opal_client/data/oauth2_updater.py | 35 ++++ .../opal-client/opal_client/data/updater.py | 44 +++-- .../opal_client/data/updater_factory.py | 70 ++++++++ .../opal-client/opal_client/policy/fetcher.py | 25 ++- .../opal-client/opal_client/policy/updater.py | 15 +- .../opal_client/policy_store/api.py | 4 +- .../opal_client/tests/data_updater_test.py | 8 +- .../server_to_client_intergation_test.py | 4 +- .../authentication/authenticator.py | 15 ++ .../authentication/authenticator_factory.py | 34 ++++ .../opal_common/authentication/authz.py | 6 +- .../opal_common/authentication/deps.py | 10 +- .../opal_common/authentication/jwk.py | 45 +++++ .../opal_common/authentication/oauth2.py | 164 ++++++++++++++++++ packages/opal-common/opal_common/config.py | 62 +++++++ .../fetcher/providers/http_fetch_provider.py | 14 +- .../opal_server/authentication/__init__.py | 0 .../authentication/authenticator.py | 18 ++ .../authentication/authenticator_factory.py | 49 ++++++ packages/opal-server/opal_server/data/api.py | 5 +- .../opal_server/policy/webhook/api.py | 4 +- packages/opal-server/opal_server/pubsub.py | 10 +- .../opal-server/opal_server/scopes/api.py | 6 +- .../opal-server/opal_server/security/jwks.py | 5 +- packages/opal-server/opal_server/server.py | 65 +++---- 29 files changed, 835 insertions(+), 99 deletions(-) create mode 100644 docker/docker-compose-with-oauth-jwt-token.yml create mode 100644 docker/docker-compose-with-oauth-opaque-token.yml create mode 100644 packages/opal-client/opal_client/data/oauth2_updater.py create mode 100644 packages/opal-client/opal_client/data/updater_factory.py create mode 100644 packages/opal-common/opal_common/authentication/authenticator.py create mode 100644 packages/opal-common/opal_common/authentication/authenticator_factory.py create mode 100644 packages/opal-common/opal_common/authentication/jwk.py create mode 100644 packages/opal-common/opal_common/authentication/oauth2.py create mode 100644 packages/opal-server/opal_server/authentication/__init__.py create mode 100644 packages/opal-server/opal_server/authentication/authenticator.py create mode 100644 packages/opal-server/opal_server/authentication/authenticator_factory.py diff --git a/docker/docker-compose-with-oauth-jwt-token.yml b/docker/docker-compose-with-oauth-jwt-token.yml new file mode 100644 index 000000000..b62197241 --- /dev/null +++ b/docker/docker-compose-with-oauth-jwt-token.yml @@ -0,0 +1,93 @@ +services: + # When scaling the opal-server to multiple nodes and/or multiple workers, we use + # a *broadcast* channel to sync between all the instances of opal-server. + # Under the hood, this channel is implemented by encode/broadcaster (see link below). + # At the moment, the broadcast channel can be either: postgresdb, redis or kafka. + # The format of the broadcaster URI string (the one we pass to opal server as `OPAL_BROADCAST_URI`) is specified here: + # https://github.com/encode/broadcaster#available-backends + broadcast_channel: + image: postgres:alpine + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + opal_server: + # by default we run opal-server from latest official image + image: permitio/opal-server:latest + environment: + # the broadcast backbone uri used by opal server workers (see comments above for: broadcast_channel) + - OPAL_BROADCAST_URI=postgres://postgres:postgres@broadcast_channel:5432/postgres + # number of uvicorn workers to run inside the opal-server container + - UVICORN_NUM_WORKERS=4 + # the git repo hosting our policy + # - if this repo is not public, you can pass an ssh key via `OPAL_POLICY_REPO_SSH_KEY`) + # - the repo we pass in this example is *public* and acts as an example repo with dummy rego policy + # - for more info, see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo + # in this example we will use a polling interval of 30 seconds to check for new policy updates (git commits affecting the rego policy). + # however, it is better to utilize a git *webhook* to trigger the server to check for changes only when the repo has new commits. + # for more info see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_POLLING_INTERVAL=30 + # configures from where the opal client should initially fetch data (when it first goes up, after disconnection, etc). + # the data sources represents from where the opal clients should get a "complete picture" of the data they need. + # after the initial sources are fetched, the client will subscribe only to update notifications sent by the server. + - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}} + - OPAL_LOG_FORMAT_INCLUDE_PID=true + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # JWT validation + - OPAL_OAUTH2_OPENID_CONFIGURATION_URL=https://example/.well-known/openid-configuration + - OPAL_OAUTH2_EXACT_MATCH_CLAIMS=aud=some_audience,iss=some_issuer + - OPAL_OAUTH2_REQUIRED_CLAIMS=sub,iat,exp + - OPAL_OAUTH2_JWT_ALGORITHM=RS256 + - OPAL_OAUTH2_JWT_AUDIENCE=some_audience + - OPAL_OAUTH2_JWT_ISSUER=https://example/issuer + ports: + # exposes opal server on the host machine, you can access the server at: http://localhost:7002 + - "7002:7002" + depends_on: + - broadcast_channel + opal_client: + # by default we run opal-client from latest official image + image: permitio/opal-client:latest + environment: + - OPAL_SERVER_URL=http://opal_server:7002 + - OPAL_LOG_FORMAT_INCLUDE_PID=true + - OPAL_INLINE_OPA_LOG_FORMAT=http + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # client credentials + - OPAL_OAUTH2_CLIENT_ID=some_client_id + - OPAL_OAUTH2_CLIENT_SECRET=some_client_secret + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # JWT validation + - OPAL_OAUTH2_OPENID_CONFIGURATION_URL=https://example/.well-known/openid-configuration + - OPAL_OAUTH2_EXACT_MATCH_CLAIMS=aud=some_audience,iss=some_issuer + - OPAL_OAUTH2_REQUIRED_CLAIMS=sub,iat,exp + - OPAL_OAUTH2_JWT_ALGORITHM=RS256 + - OPAL_OAUTH2_JWT_AUDIENCE=some_audience + - OPAL_OAUTH2_JWT_ISSUER=https://example/issuer + # Enable Authorization / Authentication in OPA + - 'OPAL_INLINE_OPA_CONFIG={"authentication":"token", "authorization":"basic", "files": ["authz.rego"]}' + volumes: + # The goal is to create an initial authorization rego that allows OPAL to write the first policy from the POLICY_REPO_URL. + # This is achieved through policy overwrite based on the "id" attribute. + # When the authz.rego file is placed in the root directory of OPA, it is given the id 'authz.rego'. + # Similarly, if there is another authz.rego file in the root of POLICY_REPO_URL, it will also be given the id 'authz.rego'. + # Therefore, if the authz.rego file from the POLICY_REPO_URL exists, it will overwrite the initial authz.rego file. + - ./docker_files/policy_test/authz.rego:/opal/authz.rego + ports: + # exposes opal client on the host machine, you can access the client at: http://localhost:7766 + - "7766:7000" + # exposes the OPA agent (being run by OPAL) on the host machine + # you can access the OPA api that you know and love at: http://localhost:8181 + # OPA api docs are at: https://www.openpolicyagent.org/docs/latest/rest-api/ + - "8181:8181" + depends_on: + - opal_server + # this command is not necessary when deploying OPAL for real, it is simply a trick for dev environments + # to make sure that opal-server is already up before starting the client. + command: sh -c "exec ./wait-for.sh opal_server:7002 --timeout=20 -- ./start.sh" diff --git a/docker/docker-compose-with-oauth-opaque-token.yml b/docker/docker-compose-with-oauth-opaque-token.yml new file mode 100644 index 000000000..7641cd0e8 --- /dev/null +++ b/docker/docker-compose-with-oauth-opaque-token.yml @@ -0,0 +1,83 @@ +services: + # When scaling the opal-server to multiple nodes and/or multiple workers, we use + # a *broadcast* channel to sync between all the instances of opal-server. + # Under the hood, this channel is implemented by encode/broadcaster (see link below). + # At the moment, the broadcast channel can be either: postgresdb, redis or kafka. + # The format of the broadcaster URI string (the one we pass to opal server as `OPAL_BROADCAST_URI`) is specified here: + # https://github.com/encode/broadcaster#available-backends + broadcast_channel: + image: postgres:alpine + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + opal_server: + # by default we run opal-server from latest official image + image: permitio/opal-server:latest + environment: + # the broadcast backbone uri used by opal server workers (see comments above for: broadcast_channel) + - OPAL_BROADCAST_URI=postgres://postgres:postgres@broadcast_channel:5432/postgres + # number of uvicorn workers to run inside the opal-server container + - UVICORN_NUM_WORKERS=4 + # the git repo hosting our policy + # - if this repo is not public, you can pass an ssh key via `OPAL_POLICY_REPO_SSH_KEY`) + # - the repo we pass in this example is *public* and acts as an example repo with dummy rego policy + # - for more info, see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo + # in this example we will use a polling interval of 30 seconds to check for new policy updates (git commits affecting the rego policy). + # however, it is better to utilize a git *webhook* to trigger the server to check for changes only when the repo has new commits. + # for more info see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_POLLING_INTERVAL=30 + # configures from where the opal client should initially fetch data (when it first goes up, after disconnection, etc). + # the data sources represents from where the opal clients should get a "complete picture" of the data they need. + # after the initial sources are fetched, the client will subscribe only to update notifications sent by the server. + - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}} + - OPAL_LOG_FORMAT_INCLUDE_PID=true + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # introspect URL for Opaque token validation + - OPAL_OAUTH2_INTROSPECT_URL=https://example/oauth2/introspect + ports: + # exposes opal server on the host machine, you can access the server at: http://localhost:7002 + - "7002:7002" + depends_on: + - broadcast_channel + opal_client: + # by default we run opal-client from latest official image + image: permitio/opal-client:latest + environment: + - OPAL_SERVER_URL=http://opal_server:7002 + - OPAL_LOG_FORMAT_INCLUDE_PID=true + - OPAL_INLINE_OPA_LOG_FORMAT=http + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # client credentials + - OPAL_OAUTH2_CLIENT_ID=some_client_id + - OPAL_OAUTH2_CLIENT_SECRET=some_client_secret + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # introspect URL for Opaque token validation + - OPAL_OAUTH2_INTROSPECT_URL=https://example/oauth2/introspect + # Enable Authorization / Authentication in OPA + - 'OPAL_INLINE_OPA_CONFIG={"authentication":"token", "authorization":"basic", "files": ["authz.rego"]}' + volumes: + # The goal is to create an initial authorization rego that allows OPAL to write the first policy from the POLICY_REPO_URL. + # This is achieved through policy overwrite based on the "id" attribute. + # When the authz.rego file is placed in the root directory of OPA, it is given the id 'authz.rego'. + # Similarly, if there is another authz.rego file in the root of POLICY_REPO_URL, it will also be given the id 'authz.rego'. + # Therefore, if the authz.rego file from the POLICY_REPO_URL exists, it will overwrite the initial authz.rego file. + - ./docker_files/policy_test/authz.rego:/opal/authz.rego + ports: + # exposes opal client on the host machine, you can access the client at: http://localhost:7766 + - "7766:7000" + # exposes the OPA agent (being run by OPAL) on the host machine + # you can access the OPA api that you know and love at: http://localhost:8181 + # OPA api docs are at: https://www.openpolicyagent.org/docs/latest/rest-api/ + - "8181:8181" + depends_on: + - opal_server + # this command is not necessary when deploying OPAL for real, it is simply a trick for dev environments + # to make sure that opal-server is already up before starting the client. + command: sh -c "exec ./wait-for.sh opal_server:7002 --timeout=20 -- ./start.sh" diff --git a/packages/opal-client/opal_client/callbacks/api.py b/packages/opal-client/opal_client/callbacks/api.py index a2e2d5a63..90b1e6ecd 100644 --- a/packages/opal-client/opal_client/callbacks/api.py +++ b/packages/opal-client/opal_client/callbacks/api.py @@ -3,8 +3,8 @@ from fastapi import APIRouter, Depends, HTTPException, Response, status from opal_client.callbacks.register import CallbacksRegister from opal_client.config import opal_client_config +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import require_peer_type -from opal_common.authentication.deps import JWTAuthenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -13,7 +13,7 @@ from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR -def init_callbacks_api(authenticator: JWTAuthenticator, register: CallbacksRegister): +def init_callbacks_api(authenticator: Authenticator, register: CallbacksRegister): async def require_listener_token(claims: JWTClaims = Depends(authenticator)): try: require_peer_type( diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 7944f65d8..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -2,9 +2,7 @@ import functools import os import signal -import tempfile import uuid -from logging import disable from typing import Awaitable, Callable, List, Literal, Optional, Union import aiofiles @@ -19,8 +17,8 @@ from opal_client.callbacks.register import CallbacksRegister from opal_client.config import PolicyStoreTypes, opal_client_config from opal_client.data.api import init_data_router -from opal_client.data.fetcher import DataFetcher from opal_client.data.updater import DataUpdater +from opal_client.data.updater_factory import DataUpdaterFactory from opal_client.engine.options import CedarServerOptions, OpaServerOptions from opal_client.engine.runner import CedarRunner, OpaRunner from opal_client.limiter import StartupLoadLimiter @@ -31,8 +29,8 @@ from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) -from opal_common.authentication.deps import JWTAuthenticator -from opal_common.authentication.verifier import JWTVerifier +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.logger import configure_logs, logger from opal_common.middleware import configure_middleware @@ -51,7 +49,7 @@ def __init__( inline_opa_options: OpaServerOptions = None, inline_cedar_enabled: bool = None, inline_cedar_options: CedarServerOptions = None, - verifier: Optional[JWTVerifier] = None, + authenticator: Optional[Authenticator] = None, store_backup_path: Optional[str] = None, store_backup_interval: Optional[int] = None, offline_mode_enabled: bool = False, @@ -70,6 +68,10 @@ def __init__( data_updater (DataUpdater, optional): Defaults to None. policy_updater (PolicyUpdater, optional): Defaults to None. """ + if authenticator is not None: + self.authenticator = authenticator + else: + self.authenticator = AuthenticatorFactory.create() self._shard_id = shard_id # defaults policy_store_type: PolicyStoreTypes = ( @@ -127,6 +129,7 @@ def __init__( opal_client_id=opal_client_identifier, on_connect=on_policy_updater_connect, on_disconnect=on_policy_updater_disconnect, + authenticator=self.authenticator, ) else: self.policy_updater = None @@ -142,7 +145,7 @@ def __init__( else opal_client_config.DATA_TOPICS ) - self.data_updater = DataUpdater( + self.data_updater = DataUpdaterFactory.create( policy_store=self.policy_store, data_topics=data_topics, callbacks_register=self._callbacks_register, @@ -150,6 +153,7 @@ def __init__( shard_id=self._shard_id, on_connect=on_data_updater_connect, on_disconnect=on_data_updater_disconnect, + authenticator=self.authenticator, ) else: self.data_updater = None @@ -172,19 +176,6 @@ def __init__( "OPAL client is configured to trust self-signed certificates" ) - if verifier is not None: - self.verifier = verifier - else: - self.verifier = JWTVerifier( - public_key=opal_common_config.AUTH_PUBLIC_KEY, - algorithm=opal_common_config.AUTH_JWT_ALGORITHM, - audience=opal_common_config.AUTH_JWT_AUDIENCE, - issuer=opal_common_config.AUTH_JWT_ISSUER, - ) - if not self.verifier.enabled: - logger.info( - "API authentication disabled (public encryption key was not provided)" - ) self.store_backup_path = ( store_backup_path or opal_client_config.STORE_BACKUP_PATH ) @@ -264,13 +255,11 @@ async def _is_ready(self): def _configure_api_routes(self, app: FastAPI): """Mounts the api routes on the app object.""" - authenticator = JWTAuthenticator(self.verifier) - # Init api routers with required dependencies policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) - policy_store_router = init_policy_store_router(authenticator) - callbacks_router = init_callbacks_api(authenticator, self._callbacks_register) + policy_store_router = init_policy_store_router(self.authenticator) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-client/opal_client/data/oauth2_updater.py b/packages/opal-client/opal_client/data/oauth2_updater.py new file mode 100644 index 000000000..12adfd47a --- /dev/null +++ b/packages/opal-client/opal_client/data/oauth2_updater.py @@ -0,0 +1,35 @@ +import aiohttp +from aiohttp.client import ClientSession +from opal_client.logger import logger +from urllib.parse import urlencode, urlparse, parse_qs + +from .updater import DefaultDataUpdater + + +class OAuth2DataUpdater(DefaultDataUpdater): + async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + await self._authenticator.authenticate(headers) + + async with ClientSession(headers=headers) as session: + response = await session.get(url, **self._ssl_context_kwargs, allow_redirects=False) + + if response.status == 307: + return await self._load_redirected_policy_data_config(response.headers['location'], headers) + else: + return response + + async def _load_redirected_policy_data_config(self, url: str, headers): + redirect_url = self.__redirect_url(url) + + logger.info("Redirecting to data-sources configuration '{source}'", source=redirect_url) + + async with ClientSession(headers=headers) as session: + return await session.get(redirect_url, **self._ssl_context_kwargs, allow_redirects=False) + + def __redirect_url(self, url: str) -> str: + u = urlparse(url) + query = parse_qs(u.query, keep_blank_values=True) + query.pop('token', None) + u = u._replace(query=urlencode(query, True)) + + return u.geturl() \ No newline at end of file diff --git a/packages/opal-client/opal_client/data/updater.py b/packages/opal-client/opal_client/data/updater.py index 2df151423..27c590ccd 100644 --- a/packages/opal-client/opal_client/data/updater.py +++ b/packages/opal-client/opal_client/data/updater.py @@ -3,7 +3,7 @@ import json import uuid from functools import partial -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import aiohttp from aiohttp.client import ClientError, ClientSession @@ -25,6 +25,8 @@ DEFAULT_POLICY_STORE_GETTER, ) from opal_common.async_utils import TasksPool, repeated_call +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.http_utils import is_http_error_response from opal_common.schemas.data import ( @@ -71,6 +73,7 @@ def __init__( shard_id: Optional[str] = None, on_connect: List[PubSubOnConnectCallback] = None, on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, ): """Initializes the DataUpdater with the necessary configuration and clients. @@ -165,6 +168,10 @@ def __init__( # Optional user-defined hooks for connection lifecycle self._on_connect_callbacks = on_connect or [] self._on_disconnect_callbacks = on_disconnect or [] + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() async def __aenter__(self): await self.start() @@ -229,20 +236,30 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: url = self._data_sources_config_url logger.info("Getting data-sources configuration from '{source}'", source=url) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + headers['Accept'] = "application/json" + try: - async with ClientSession(headers=self._extra_headers) as session: - response = await session.get(url, **self._ssl_context_kwargs) - if response.status == 200: - return DataSourceConfig.parse_obj(await response.json()) - else: - error_details = await response.json() - raise ClientError( - f"Fetch data sources failed with status code {response.status}, error: {error_details}" - ) + response = await self._load_policy_data_config(url, headers) + + if response.status == 200: + return DataSourceConfig.parse_obj(await response.json()) + else: + error_details = await response.text() + raise ClientError( + f"Fetch data sources failed with status code {response.status}, error: {error_details}" + ) except: logger.exception("Failed to load data sources config") raise + async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async with ClientSession(headers=headers) as session: + return await session.get(url, **self._ssl_context_kwargs) + async def get_base_policy_data( self, config_url: str = None, data_fetch_reason="Initial load" ): @@ -341,6 +358,12 @@ async def _subscriber(self): callback. """ logger.info("Subscribing to topics: {topics}", topics=self._data_topics) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + await self._authenticator.authenticate(headers) + self._client = PubSubClient( self._data_topics, self._update_policy_data_callback, @@ -348,6 +371,7 @@ async def _subscriber(self): on_connect=[self.on_connect, *self._on_connect_callbacks], on_disconnect=[self.on_disconnect, *self._on_disconnect_callbacks], additional_headers=self._extra_headers, + extra_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, diff --git a/packages/opal-client/opal_client/data/updater_factory.py b/packages/opal-client/opal_client/data/updater_factory.py new file mode 100644 index 000000000..ad784f996 --- /dev/null +++ b/packages/opal-client/opal_client/data/updater_factory.py @@ -0,0 +1,70 @@ +from typing import List, Optional + +from fastapi_websocket_pubsub.pub_sub_client import PubSubOnConnectCallback +from fastapi_websocket_rpc.rpc_channel import OnDisconnectCallback +from opal_client.callbacks.register import CallbacksRegister +from opal_client.data.fetcher import DataFetcher +from opal_client.policy_store.base_policy_store_client import BasePolicyStoreClient +from opal_common.authentication.authenticator import Authenticator +from opal_common.config import opal_common_config +from opal_common.logger import logger + +from .oauth2_updater import OAuth2DataUpdater +from .updater import DataUpdater, DefaultDataUpdater + + +class DataUpdaterFactory: + @staticmethod + def create( + token: str = None, + pubsub_url: str = None, + data_sources_config_url: str = None, + fetch_on_connect: bool = True, + data_topics: List[str] = None, + policy_store: BasePolicyStoreClient = None, + should_send_reports=None, + data_fetcher: Optional[DataFetcher] = None, + callbacks_register: Optional[CallbacksRegister] = None, + opal_client_id: str = None, + shard_id: Optional[str] = None, + on_connect: List[PubSubOnConnectCallback] = None, + on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, + ) -> DataUpdater: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will authenticate Datasource requests with OAuth2 tokens." + ) + return OAuth2DataUpdater( + token, + pubsub_url, + data_sources_config_url, + fetch_on_connect, + data_topics, + policy_store, + should_send_reports, + data_fetcher, + callbacks_register, + opal_client_id, + shard_id, + on_connect, + on_disconnect, + authenticator, + ) + else: + return DefaultDataUpdater( + token, + pubsub_url, + data_sources_config_url, + fetch_on_connect, + data_topics, + policy_store, + should_send_reports, + data_fetcher, + callbacks_register, + opal_client_id, + shard_id, + on_connect, + on_disconnect, + authenticator, + ) diff --git a/packages/opal-client/opal_client/policy/fetcher.py b/packages/opal-client/opal_client/policy/fetcher.py index b7c8c543f..13e6424cb 100644 --- a/packages/opal-client/opal_client/policy/fetcher.py +++ b/packages/opal-client/opal_client/policy/fetcher.py @@ -4,6 +4,8 @@ from fastapi import HTTPException, status from opal_client.config import opal_client_config from opal_client.logger import logger +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.schemas.policy import PolicyBundle from opal_common.security.sslcontext import get_custom_ssl_context from opal_common.utils import ( @@ -28,15 +30,27 @@ def force_valid_bundle(bundle) -> PolicyBundle: class PolicyFetcher: """Fetches policy from backend.""" - def __init__(self, backend_url=None, token=None): + def __init__( + self, + backend_url=None, + token=None, + authenticator: Optional[Authenticator] = None, + ): """ Args: backend_url (str): Defaults to opal_client_config.SERVER_URL. token ([type], optional): [description]. Defaults to opal_client_config.CLIENT_TOKEN. """ + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() self._token = token or opal_client_config.CLIENT_TOKEN self._backend_url = backend_url or opal_client_config.SERVER_URL - self._auth_headers = tuple_to_dict(get_authorization_header(self._token)) + if self._token is not None: + self._auth_headers = tuple_to_dict(get_authorization_header(self._token)) + else: + self._auth_headers = dict() self._retry_config = ( opal_client_config.POLICY_UPDATER_CONN_RETRY.toTenacityConfig() @@ -82,10 +96,15 @@ async def _fetch_policy_bundle( May throw, in which case we retry again. """ + headers = {} + if self._auth_headers is not None: + headers = self._auth_headers.copy() + await self._authenticator.authenticate(headers) + params = {"path": directories} if base_hash is not None: params["base_hash"] = base_hash - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(headers=headers) as session: logger.info( "Fetching policy bundle from {url}", url=self._policy_endpoint_url, diff --git a/packages/opal-client/opal_client/policy/updater.py b/packages/opal-client/opal_client/policy/updater.py index b2e56f3d8..07c57c6a4 100644 --- a/packages/opal-client/opal_client/policy/updater.py +++ b/packages/opal-client/opal_client/policy/updater.py @@ -46,6 +46,7 @@ def __init__( opal_client_id: str = None, on_connect: List[PubSubOnConnectCallback] = None, on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, ): """Inits the policy updater. @@ -67,6 +68,10 @@ def __init__( self._opal_client_id = opal_client_id self._scope_id = opal_client_config.SCOPE_ID + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() # The policy store we'll save policy modules into (i.e: OPA) self._policy_store = policy_store or DEFAULT_POLICY_STORE_GETTER() # pub/sub server url and authentication data @@ -90,7 +95,7 @@ def __init__( self._policy_update_task = None self._stopping = False # policy fetcher - fetches policy bundles - self._policy_fetcher = PolicyFetcher() + self._policy_fetcher = PolicyFetcher(authenticator=self._authenticator) # callbacks on policy changes self._data_fetcher = data_fetcher or DataFetcher() self._callbacks_register = callbacks_register or CallbacksRegister() @@ -245,12 +250,18 @@ async def _subscriber(self): update_policy() callback (which will fetch the relevant policy bundle from the server and update the policy store).""" logger.info("Subscribing to topics: {topics}", topics=self._topics) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + await self._authenticator.authenticate(headers) + self._client = PubSubClient( topics=self._topics, callback=self._update_policy_callback, on_connect=[self._on_connect, *self._on_connect_callbacks], on_disconnect=[self._on_disconnect, *self._on_disconnect_callbacks], - additional_headers=self._extra_headers, + extra_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, diff --git a/packages/opal-client/opal_client/policy_store/api.py b/packages/opal-client/opal_client/policy_store/api.py index b27d83d70..97113f109 100644 --- a/packages/opal-client/opal_client/policy_store/api.py +++ b/packages/opal-client/opal_client/policy_store/api.py @@ -1,15 +1,15 @@ from fastapi import APIRouter, Depends from opal_client.config import opal_client_config from opal_client.policy_store.schemas import PolicyStoreAuth, PolicyStoreDetails +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import require_peer_type -from opal_common.authentication.deps import JWTAuthenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger from opal_common.schemas.security import PeerType -def init_policy_store_router(authenticator: JWTAuthenticator): +def init_policy_store_router(authenticator: Authenticator): router = APIRouter() @router.get( diff --git a/packages/opal-client/opal_client/tests/data_updater_test.py b/packages/opal-client/opal_client/tests/data_updater_test.py index cad760a10..eccd82a97 100644 --- a/packages/opal-client/opal_client/tests/data_updater_test.py +++ b/packages/opal-client/opal_client/tests/data_updater_test.py @@ -21,7 +21,7 @@ from opal_client.config import opal_client_config from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater +from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater, DefaultDataUpdater from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) @@ -171,7 +171,7 @@ async def test_data_updater(server): server trigger a Data-update and check our policy store gets the update.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, policy_store=policy_store, fetch_on_connect=False, @@ -251,7 +251,7 @@ async def test_data_updater_with_report_callback(server): server trigger a Data-update and check our policy store gets the update.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, policy_store=policy_store, fetch_on_connect=False, @@ -311,7 +311,7 @@ async def test_client_get_initial_data(server): """Connect to OPAL-server and make sure data is fetched on-connect.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, data_sources_config_url=DATA_CONFIG_URL, policy_store=policy_store, diff --git a/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py b/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py index a3372c56f..5ce829fd7 100644 --- a/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py +++ b/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py @@ -18,7 +18,7 @@ from opal_client import OpalClient from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater +from opal_client.data.updater import DataSourceEntry, DataUpdate, DefaultDataUpdater from opal_client.policy_store.mock_policy_store_client import MockPolicyStoreClient from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, @@ -76,7 +76,7 @@ async def startup_event(): def setup_client(event): # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - data_updater = DataUpdater( + data_updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, data_sources_config_url=DATA_CONFIG_URL, policy_store=policy_store, diff --git a/packages/opal-common/opal_common/authentication/authenticator.py b/packages/opal-common/opal_common/authentication/authenticator.py new file mode 100644 index 000000000..87e210ad4 --- /dev/null +++ b/packages/opal-common/opal_common/authentication/authenticator.py @@ -0,0 +1,15 @@ +from typing import Optional + +from opal_common.authentication.signer import JWTSigner + + +class Authenticator: + @property + def enabled(self) -> bool: + raise NotImplementedError() + + def signer(self) -> Optional[JWTSigner]: + raise NotImplementedError() + + async def authenticate(self, headers): + raise NotImplementedError() diff --git a/packages/opal-common/opal_common/authentication/authenticator_factory.py b/packages/opal-common/opal_common/authentication/authenticator_factory.py new file mode 100644 index 000000000..c0bcc40ad --- /dev/null +++ b/packages/opal-common/opal_common/authentication/authenticator_factory.py @@ -0,0 +1,34 @@ +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.verifier import JWTVerifier, Unauthorized +from opal_common.config import opal_common_config +from opal_common.logger import logger + +from .oauth2 import CachedOAuth2Authenticator, OAuth2ClientCredentialsAuthenticator + + +class AuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will authenticate API requests with OAuth2 tokens." + ) + return CachedOAuth2Authenticator(OAuth2ClientCredentialsAuthenticator()) + else: + return JWTAuthenticator(AuthenticatorFactory.__verifier()) + + @staticmethod + def __verifier() -> JWTVerifier: + verifier = JWTVerifier( + public_key=opal_common_config.AUTH_PUBLIC_KEY, + algorithm=opal_common_config.AUTH_JWT_ALGORITHM, + audience=opal_common_config.AUTH_JWT_AUDIENCE, + issuer=opal_common_config.AUTH_JWT_ISSUER, + ) + if not verifier.enabled: + logger.info( + "API authentication disabled (public encryption key was not provided)" + ) + + return verifier diff --git a/packages/opal-common/opal_common/authentication/authz.py b/packages/opal-common/opal_common/authentication/authz.py index 742304bf5..822497e64 100644 --- a/packages/opal-common/opal_common/authentication/authz.py +++ b/packages/opal-common/opal_common/authentication/authz.py @@ -1,4 +1,4 @@ -from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.schemas.data import DataUpdate @@ -6,7 +6,7 @@ def require_peer_type( - authenticator: JWTAuthenticator, claims: JWTClaims, required_type: PeerType + authenticator: Authenticator, claims: JWTClaims, required_type: PeerType ): if not authenticator.enabled: return @@ -28,7 +28,7 @@ def require_peer_type( def restrict_optional_topics_to_publish( - authenticator: JWTAuthenticator, claims: JWTClaims, update: DataUpdate + authenticator: Authenticator, claims: JWTClaims, update: DataUpdate ): if not authenticator.enabled: return diff --git a/packages/opal-common/opal_common/authentication/deps.py b/packages/opal-common/opal_common/authentication/deps.py index 2ec63043b..332a37cb7 100644 --- a/packages/opal-common/opal_common/authentication/deps.py +++ b/packages/opal-common/opal_common/authentication/deps.py @@ -4,6 +4,8 @@ from fastapi import Header from fastapi.exceptions import HTTPException from fastapi.security.utils import get_authorization_scheme_param +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.signer import JWTSigner from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.logger import logger @@ -67,7 +69,7 @@ def verify_logged_in(verifier: JWTVerifier, token: Optional[str]) -> JWTClaims: raise -class _JWTAuthenticator: +class _JWTAuthenticator(Authenticator): def __init__(self, verifier: JWTVerifier): self._verifier = verifier @@ -75,10 +77,16 @@ def __init__(self, verifier: JWTVerifier): def verifier(self) -> JWTVerifier: return self._verifier + def signer(self) -> Optional[JWTSigner]: + return self._verifier + @property def enabled(self) -> JWTVerifier: return self._verifier.enabled + async def authenticate(self, headers): + pass + class JWTAuthenticator(_JWTAuthenticator): """Bearer token authentication for http(s) api endpoints. diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py new file mode 100644 index 000000000..182b5cdb9 --- /dev/null +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -0,0 +1,45 @@ +import jwt +import httpx + +from cachetools import TTLCache +from opal_common.authentication.verifier import Unauthorized + +class JWKManager: + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + self._openid_configuration_url = openid_configuration_url + self._jwt_algorithm = jwt_algorithm + self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) + + def public_key(self, token): + header = jwt.get_unverified_header(token) + kid = header['kid'] + + public_key = self._cache.get(kid) + if public_key is None: + public_key = self._fetch_public_key(token) + self._cache[kid] = public_key + + return public_key + + def _fetch_public_key(self, token: str): + try: + return self._jwks_client().get_signing_key_from_jwt(token).key + except Exception: + raise Unauthorized(description="unknown JWT error") + + def _jwks_client(self): + oidc_config = self._openid_configuration() + signing_algorithms = oidc_config["id_token_signing_alg_values_supported"] + if self._jwt_algorithm.name not in signing_algorithms: + raise Unauthorized(description="unknown JWT algorithm") + if "jwks_uri" not in oidc_config: + raise Unauthorized(description="missing 'jwks_uri' property") + return jwt.PyJWKClient(oidc_config["jwks_uri"]) + + def _openid_configuration(self): + response = httpx.get(self._openid_configuration_url) + + if response.status_code != httpx.codes.OK: + raise Unauthorized(description=f"invalid status code {response.status_code}") + + return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py new file mode 100644 index 000000000..33bc4647a --- /dev/null +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -0,0 +1,164 @@ +import asyncio +import httpx +import time + +from cachetools import cached, TTLCache +from fastapi import Header +from httpx import AsyncClient, BasicAuth +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import get_token_from_header +from opal_common.authentication.jwk import JWKManager +from opal_common.authentication.signer import JWTSigner +from opal_common.authentication.verifier import JWTVerifier, Unauthorized +from opal_common.config import opal_common_config +from typing import Optional + +class _OAuth2Authenticator(Authenticator): + async def authenticate(self, headers): + if "Authorization" not in headers: + token = await self.token() + headers['Authorization'] = f"Bearer {token}" + + +class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): + def __init__(self) -> None: + self._client_id = opal_common_config.OAUTH2_CLIENT_ID + self._client_secret = opal_common_config.OAUTH2_CLIENT_SECRET + self._token_url = opal_common_config.OAUTH2_TOKEN_URL + self._introspect_url = opal_common_config.OAUTH2_INTROSPECT_URL + self._jwt_algorithm = opal_common_config.OAUTH2_JWT_ALGORITHM + self._jwt_audience = opal_common_config.OAUTH2_JWT_AUDIENCE + self._jwt_issuer = opal_common_config.OAUTH2_JWT_ISSUER + self._jwk_manager = JWKManager( + opal_common_config.OAUTH2_OPENID_CONFIGURATION_URL, + opal_common_config.OAUTH2_JWT_ALGORITHM, + opal_common_config.OAUTH2_JWK_CACHE_MAXSIZE, + opal_common_config.OAUTH2_JWK_CACHE_TTL, + ) + + cfg = opal_common_config.OAUTH2_EXACT_MATCH_CLAIMS + if cfg is None: + self._exact_match_claims = {} + else: + self._exact_match_claims = dict(map(lambda x: x.split("="), cfg.split(","))) + + cfg = opal_common_config.OAUTH2_REQUIRED_CLAIMS + if cfg is None: + self._required_claims = [] + else: + self._required_claims = cfg.split(",") + + @property + def enabled(self): + return True + + def signer(self) -> Optional[JWTSigner]: + return None + + async def token(self): + auth = BasicAuth(self._client_id, self._client_secret) + data = {"grant_type": "client_credentials"} + + async with AsyncClient() as client: + response = await client.post(self._token_url, auth=auth, data=data) + return (response.json())['access_token'] + + def __call__(self, authorization: Optional[str] = Header(None)) -> {}: + token = get_token_from_header(authorization) + return self.verify(token) + + def verify(self, token: str) -> {}: + if self._introspect_url is not None: + claims = self._verify_opaque(token) + else: + claims = self._verify_jwt(token) + + self._verify_exact_match_claims(claims) + self._verify_required_claims(claims) + + return claims + + def _verify_opaque(self, token: str) -> {}: + response = httpx.post(self._introspect_url, data={'token': token}) + + if response.status_code != httpx.codes.OK: + raise Unauthorized(description=f"invalid status code {response.status_code}") + + claims = response.json() + active = claims.get("active", False) + if not active: + raise Unauthorized(description="inactive token") + + return claims or {} + + def _verify_jwt(self, token: str) -> {}: + public_key = self._jwk_manager.public_key(token) + + verifier = JWTVerifier( + public_key=public_key, + algorithm=self._jwt_algorithm, + audience=self._jwt_audience, + issuer=self._jwt_issuer, + ) + claims = verifier.verify(token) + + return claims or {} + + def _verify_exact_match_claims(self, claims): + for key, value in self._exact_match_claims.items(): + if key not in claims: + raise Unauthorized(description=f"missing required '{key}' claim") + elif claims[key] != value: + raise Unauthorized(description=f"invalid '{key}' claim value") + + def _verify_required_claims(self, claims): + for claim in self._required_claims: + if claim not in claims: + raise Unauthorized(description=f"missing required '{claim}' claim") + + +class CachedOAuth2Authenticator(_OAuth2Authenticator): + lock = asyncio.Lock() + + def __init__(self, delegate: OAuth2ClientCredentialsAuthenticator) -> None: + self._token = None + self._exp = None + self._exp_margin = opal_common_config.OAUTH2_EXP_MARGIN + self._delegate = delegate + + @property + def enabled(self): + return self._delegate.enabled + + def signer(self) -> Optional[JWTSigner]: + return self._delegate.signer() + + def _expired(self): + if self._token is None: + return True + + now = int(time.time()) + return now > self._exp - self._exp_margin + + async def token(self): + if not self._expired(): + return self._token + + async with CachedOAuth2Authenticator.lock: + if not self._expired(): + return self._token + + token = await self._delegate.token() + claims = self._delegate.verify(token) + + self._token = token + self._exp = claims['exp'] + + return self._token + + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) + def __call__(self, authorization: Optional[str] = Header(None)) -> {}: + return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index b4f9f5b8a..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -191,6 +191,68 @@ class OpalCommonConfig(Confi): [".rego"], description="List of extensions to serve as policy modules", ) + AUTH_TYPE = confi.str( + "AUTH_TYPE", + None, + description="Authentication type. Available options are oauth2 for validating access token via either OAUTH2_INTROSPECT_URL or OPAL_OAUTH2_OPENID_CONFIGURATION_URL or anything else if you prefer OPAL to do the job.", + ) + OAUTH2_CLIENT_ID = confi.str( + "OAUTH2_CLIENT_ID", None, description="OAuth2 Client ID." + ) + OAUTH2_CLIENT_SECRET = confi.str( + "OAUTH2_CLIENT_SECRET", None, description="OAuth2 Client Secret." + ) + OAUTH2_TOKEN_URL = confi.str( + "OAUTH2_TOKEN_URL", None, description="OAuth2 Token URL." + ) + OAUTH2_INTROSPECT_URL = confi.str( + "OAUTH2_INTROSPECT_URL", None, description="OAuth2 introspect URL." + ) + OAUTH2_OPENID_CONFIGURATION_URL = confi.str( + "OAUTH2_OPENID_CONFIGURATION_URL", + None, + description="OAuth2 OpenID configuration URL.", + ) + OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE = confi.int( + "OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE", + 100, + description="OAuth2 token validation cache maxsize.", + ) + OAUTH2_TOKEN_VERIFY_CACHE_TTL = confi.int( + "OAUTH2_TOKEN_VERIFY_CACHE_TTL", + 5 * 60, + description="OAuth2 token validation cache TTL.", + ) + + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) + OAUTH2_JWT_ALGORITHM = confi.enum( + "OAUTH2_JWT_ALGORITHM", + JWTAlgorithm, + getattr(JWTAlgorithm, "RS256"), + description="jwt algorithm, possible values: see: https://pyjwt.readthedocs.io/en/stable/algorithms.html", + ) + OAUTH2_JWT_AUDIENCE = confi.str( + "OAUTH2_JWT_AUDIENCE", None, description="OAuth2 required audience" + ) + OAUTH2_JWT_ISSUER = confi.str( + "OAUTH2_JWT_ISSUER", None, description="OAuth2 required issuer" + ) + OAUTH2_JWK_CACHE_MAXSIZE = confi.int( + "OAUTH2_JWK_CACHE_MAXSIZE", 100, description="OAuth2 JWKS cache maxsize." + ) + OAUTH2_JWK_CACHE_TTL = confi.int( + "OAUTH2_JWK_CACHE_TTL", 7 * 24 * 60 * 60, description="OAuth2 JWKS cache TTL." + ) ENABLE_METRICS = confi.bool( "ENABLE_METRICS", False, description="Enable metrics collection" diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index fc74223ed..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -1,10 +1,12 @@ """Simple HTTP get data fetcher using requests supports.""" from enum import Enum -from typing import Any, Union, cast +from typing import Any, Optional, Union, cast import httpx from aiohttp import ClientResponse, ClientSession +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.fetcher.events import FetcherConfig, FetchEvent from opal_common.fetcher.fetch_provider import BaseFetchProvider @@ -52,6 +54,8 @@ class HttpFetchEvent(FetchEvent): class HttpFetchProvider(BaseFetchProvider): + _authenticator: Optional[Authenticator] = None + def __init__(self, event: HttpFetchEvent) -> None: self._event: HttpFetchEvent if event.config is None: @@ -64,6 +68,9 @@ def __init__(self, event: HttpFetchEvent) -> None: if self._custom_ssl_context is not None else {} ) + if HttpFetchProvider._authenticator is None: + HttpFetchProvider._authenticator = AuthenticatorFactory.create() + self._authenticator = HttpFetchProvider._authenticator def parse_event(self, event: FetchEvent) -> HttpFetchEvent: return HttpFetchEvent(**event.dict(exclude={"config"}), config=event.config) @@ -71,7 +78,10 @@ def parse_event(self, event: FetchEvent) -> HttpFetchEvent: async def __aenter__(self): headers = {} if self._event.config.headers is not None: - headers = self._event.config.headers + headers = self._event.config.headers.copy() + + await self._authenticator.authenticate(headers) + if opal_common_config.HTTP_FETCHER_PROVIDER_CLIENT == "httpx": self._session = httpx.AsyncClient(headers=headers) else: diff --git a/packages/opal-server/opal_server/authentication/__init__.py b/packages/opal-server/opal_server/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/opal-server/opal_server/authentication/authenticator.py b/packages/opal-server/opal_server/authentication/authenticator.py new file mode 100644 index 000000000..def646382 --- /dev/null +++ b/packages/opal-server/opal_server/authentication/authenticator.py @@ -0,0 +1,18 @@ +from typing import Optional + +from fastapi import Header +from fastapi.exceptions import HTTPException +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.types import JWTClaims +from opal_common.authentication.verifier import JWTVerifier, Unauthorized + + +class WebsocketServerAuthenticator(Authenticator): + def __init__(self, delegate: Authenticator) -> None: + self._delegate = delegate + + def __call__(self, authorization: Optional[str] = Header(None)) -> JWTClaims: + try: + return self._delegate(authorization) + except (Unauthorized, HTTPException): + return None diff --git a/packages/opal-server/opal_server/authentication/authenticator_factory.py b/packages/opal-server/opal_server/authentication/authenticator_factory.py new file mode 100644 index 000000000..43f4f092f --- /dev/null +++ b/packages/opal-server/opal_server/authentication/authenticator_factory.py @@ -0,0 +1,49 @@ +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.oauth2 import ( + CachedOAuth2Authenticator, + OAuth2ClientCredentialsAuthenticator, +) +from opal_common.authentication.signer import JWTSigner +from opal_common.config import opal_common_config +from opal_common.logger import logger +from opal_server.config import opal_server_config + +from .authenticator import WebsocketServerAuthenticator + + +class ServerAuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will verify API requests with OAuth2 tokens." + ) + return CachedOAuth2Authenticator(OAuth2ClientCredentialsAuthenticator()) + else: + return JWTAuthenticator(ServerAuthenticatorFactory.__signer()) + + @staticmethod + def __signer() -> JWTSigner: + signer = JWTSigner( + private_key=opal_server_config.AUTH_PRIVATE_KEY, + public_key=opal_common_config.AUTH_PUBLIC_KEY, + algorithm=opal_common_config.AUTH_JWT_ALGORITHM, + audience=opal_common_config.AUTH_JWT_AUDIENCE, + issuer=opal_common_config.AUTH_JWT_ISSUER, + ) + if signer.enabled: + logger.info( + "OPAL is running in secure mode - will verify API requests with JWT tokens." + ) + else: + logger.info( + "OPAL was not provided with JWT encryption keys, cannot verify api requests!" + ) + return signer + + +class WebsocketServerAuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + return WebsocketServerAuthenticator(ServerAuthenticatorFactory.create()) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index da5d043a9..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -6,7 +6,8 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.deps import JWTAuthenticator, get_token_from_header +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -25,7 +26,7 @@ def init_data_updates_router( data_update_publisher: DataUpdatePublisher, data_sources_config: ServerDataSourceConfig, - authenticator: JWTAuthenticator, + authenticator: Authenticator, ): router = APIRouter() diff --git a/packages/opal-server/opal_server/policy/webhook/api.py b/packages/opal-server/opal_server/policy/webhook/api.py index c19595ad2..ef54c81b4 100644 --- a/packages/opal-server/opal_server/policy/webhook/api.py +++ b/packages/opal-server/opal_server/policy/webhook/api.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, Request, status from fastapi_websocket_pubsub.pub_sub_server import PubSubEndpoint -from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.authenticator import Authenticator from opal_common.logger import logger from opal_common.schemas.webhook import GitWebhookRequestParams from opal_server.config import PolicySourceTypes, opal_server_config @@ -15,7 +15,7 @@ def init_git_webhook_router( - pubsub_endpoint: PubSubEndpoint, authenticator: JWTAuthenticator + pubsub_endpoint: PubSubEndpoint, authenticator: Authenticator ): async def dummy_affected_repo_urls(request: Request) -> List[str]: return [] diff --git a/packages/opal-server/opal_server/pubsub.py b/packages/opal-server/opal_server/pubsub.py index c7a3b875e..a5139db92 100644 --- a/packages/opal-server/opal_server/pubsub.py +++ b/packages/opal-server/opal_server/pubsub.py @@ -21,13 +21,12 @@ WebSocketRpcEventNotifier, ) from fastapi_websocket_rpc import RpcChannel -from opal_common.authentication.deps import WebsocketJWTAuthenticator -from opal_common.authentication.signer import JWTSigner from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.confi.confi import load_conf_if_none from opal_common.config import opal_common_config from opal_common.logger import logger +from opal_server.authentication.authenticator import WebsocketServerAuthenticator from opal_server.config import opal_server_config from pydantic import BaseModel from starlette.datastructures import QueryParams @@ -121,7 +120,11 @@ class PubSub: """Wrapper for the Pub/Sub channel used for both policy and data updates.""" - def __init__(self, signer: JWTSigner, broadcaster_uri: str = None): + def __init__( + self, + broadcaster_uri: str = None, + authenticator: Optional[WebsocketServerAuthenticator] = None, + ): """ Args: broadcaster_uri (str, optional): Which server/medium should the PubSub use for broadcasting. Defaults to BROADCAST_URI. @@ -159,7 +162,6 @@ def __init__(self, signer: JWTSigner, broadcaster_uri: str = None): not opal_server_config.BROADCAST_CONN_LOSS_BUGFIX_EXPERIMENT_ENABLED ), ) - authenticator = WebsocketJWTAuthenticator(signer) @self.api_router.get( "/pubsub_client_info", response_model=Dict[str, ClientInfo] diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index d54b1074c..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -16,12 +16,14 @@ from fastapi_websocket_pubsub import PubSubEndpoint from git import InvalidGitRepositoryError from opal_common.async_utils import run_sync +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key -from opal_common.authentication.deps import JWTAuthenticator, get_token_from_header +from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -78,7 +80,7 @@ def verify_private_key_or_throw(scope_in: Scope): def init_scope_router( scopes: ScopeRepository, - authenticator: JWTAuthenticator, + authenticator: Authenticator, pubsub_endpoint: PubSubEndpoint, ): router = APIRouter() diff --git a/packages/opal-server/opal_server/security/jwks.py b/packages/opal-server/opal_server/security/jwks.py index 8f19591b9..e128eb903 100644 --- a/packages/opal-server/opal_server/security/jwks.py +++ b/packages/opal-server/opal_server/security/jwks.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from typing import Optional from fastapi import FastAPI from fastapi.staticfiles import StaticFiles @@ -11,7 +12,7 @@ class JwksStaticEndpoint: def __init__( self, - signer: JWTSigner, + signer: Optional[JWTSigner], jwks_url: str, jwks_static_dir: str, ): @@ -25,7 +26,7 @@ def configure_app(self, app: FastAPI): # get the jwks contents from the signer jwks_contents = {} - if self._signer.enabled: + if self._signer is not None and self._signer.enabled: jwk = json.loads(self._signer.get_jwk()) jwks_contents = {"keys": [jwk]} diff --git a/packages/opal-server/opal_server/server.py b/packages/opal-server/opal_server/server.py index f320484ea..397f28446 100644 --- a/packages/opal-server/opal_server/server.py +++ b/packages/opal-server/opal_server/server.py @@ -8,8 +8,8 @@ from fastapi import Depends, FastAPI from fastapi_websocket_pubsub.event_broadcaster import EventBroadcasterContextManager -from opal_common.authentication.deps import JWTAuthenticator, StaticBearerAuthenticator -from opal_common.authentication.signer import JWTSigner +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import StaticBearerAuthenticator from opal_common.confi.confi import load_conf_if_none from opal_common.config import opal_common_config from opal_common.logger import configure_logs, logger @@ -22,6 +22,11 @@ ServerSideTopicPublisher, TopicPublisher, ) +from opal_server.authentication.authenticator import WebsocketServerAuthenticator +from opal_server.authentication.authenticator_factory import ( + ServerAuthenticatorFactory, + WebsocketServerAuthenticatorFactory, +) from opal_server.config import opal_server_config from opal_server.data.api import init_data_updates_router from opal_server.data.data_update_publisher import DataUpdatePublisher @@ -49,7 +54,8 @@ def __init__( init_publisher: bool = None, data_sources_config: Optional[ServerDataSourceConfig] = None, broadcaster_uri: str = None, - signer: Optional[JWTSigner] = None, + authenticator: Optional[Authenticator] = None, + websocketAuthenticator: Optional[WebsocketServerAuthenticator] = None, enable_jwks_endpoint=True, jwks_url: str = None, jwks_static_dir: str = None, @@ -117,33 +123,26 @@ def __init__( self.broadcaster_uri = broadcaster_uri self.master_token = master_token - if signer is not None: - self.signer = signer - else: - self.signer = JWTSigner( - private_key=opal_server_config.AUTH_PRIVATE_KEY, - public_key=opal_common_config.AUTH_PUBLIC_KEY, - algorithm=opal_common_config.AUTH_JWT_ALGORITHM, - audience=opal_common_config.AUTH_JWT_AUDIENCE, - issuer=opal_common_config.AUTH_JWT_ISSUER, - ) - if self.signer.enabled: - logger.info( - "OPAL is running in secure mode - will verify API requests with JWT tokens." - ) + if authenticator is not None: + self.authenticator = authenticator else: - logger.info( - "OPAL was not provided with JWT encryption keys, cannot verify api requests!" - ) + self.authenticator = ServerAuthenticatorFactory.create() if enable_jwks_endpoint: self.jwks_endpoint = JwksStaticEndpoint( - signer=self.signer, jwks_url=jwks_url, jwks_static_dir=jwks_static_dir + signer=self.authenticator.signer(), + jwks_url=jwks_url, + jwks_static_dir=jwks_static_dir, ) else: self.jwks_endpoint = None - self.pubsub = PubSub(signer=self.signer, broadcaster_uri=broadcaster_uri) + _websocketAuthenticator = websocketAuthenticator + if _websocketAuthenticator is None: + _websocketAuthenticator = WebsocketServerAuthenticatorFactory.create() + self.pubsub = PubSub( + broadcaster_uri=broadcaster_uri, authenticator=_websocketAuthenticator + ) self.publisher: Optional[TopicPublisher] = None self.broadcast_keepalive: Optional[PeriodicPublisher] = None @@ -219,19 +218,19 @@ def _configure_monitoring(self): def _configure_api_routes(self, app: FastAPI): """Mounts the api routes on the app object.""" - authenticator = JWTAuthenticator(self.signer) - data_update_publisher: Optional[DataUpdatePublisher] = None if self.publisher is not None: data_update_publisher = DataUpdatePublisher(self.publisher) # Init api routers with required dependencies data_updates_router = init_data_updates_router( - data_update_publisher, self.data_sources_config, authenticator + data_update_publisher, self.data_sources_config, self.authenticator + ) + webhook_router = init_git_webhook_router( + self.pubsub.endpoint, self.authenticator ) - webhook_router = init_git_webhook_router(self.pubsub.endpoint, authenticator) security_router = init_security_router( - self.signer, StaticBearerAuthenticator(self.master_token) + self.authenticator.signer(), StaticBearerAuthenticator(self.master_token) ) statistics_router = init_statistics_router(self.opal_statistics) loadlimit_router = init_loadlimit_router(self.loadlimit_notation) @@ -240,7 +239,7 @@ def _configure_api_routes(self, app: FastAPI): app.include_router( bundles_router, tags=["Bundle Server"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router(data_updates_router, tags=["Data Updates"]) app.include_router(webhook_router, tags=["Github Webhook"]) @@ -249,22 +248,24 @@ def _configure_api_routes(self, app: FastAPI): app.include_router( self.pubsub.api_router, tags=["Pub/Sub"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router( statistics_router, tags=["Server Statistics"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router( loadlimit_router, tags=["Client Load Limiting"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) if opal_server_config.SCOPES: app.include_router( - init_scope_router(self._scopes, authenticator, self.pubsub.endpoint), + init_scope_router( + self._scopes, self.authenticator, self.pubsub.endpoint + ), tags=["Scopes"], prefix="/scopes", ) From f02094e9e3924ba39273536be0c2647d40ffd474 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 002/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 481dee787eff0beef5dc3b1f2bf656ac6e2e88c3 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 003/197] Enable OAuth2 authentication. --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d9e970c8041631152f80b948ed87ced75f4456e6 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 004/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b568007f3431f224ad2be182f64ea4e39105259d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 005/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 80ba7c8d0234f3100c6206e9844b4e1525b15803 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 006/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 4d8d8aab6013ebe470a40da4f50cb5446e272d41 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 007/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5ef9717b98d1d0e0936f9c8a612f78958c0f2b01 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 008/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2e868eeae2aa652fb2859f9defeed93105fc11c8 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 009/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 04bece53ff9d836d05263bdd36fd5fdd2c464712 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 010/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3323c1c33777f2cc9465b2197f228caccde65736 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 011/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e55b6af3393a42fad25cd35f6230ce9ff9acc117 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 012/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 7cedecec8adbbd8ed8e80fe0be5b72600a62e890 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 013/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 7546031fd2fbdd876a6920810fabbee5e38df694 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 014/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b2935cef79a6048682c5b6fb724d796fcf3956f4 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 015/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 82084089f3b86626e88d139a72bc91751b016f1d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 016/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 793298c2162398d002a2f44a3e8b2a32e72cefe9 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 017/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 1197c6f120c4c9de38ca1660a166bdba0ae0a0d4 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 018/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From a108c574b15d52cea2f912c7542b5fe423e3023b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 019/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d73280fc8ad51403f1d4c0744500a2a2f4bcfbcb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 020/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d51a7d75dd337f27e9082c656eac71ccc3054ea1 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 021/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 1a1f9d780288eb91f8163286785e8f766f3940d5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 022/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b1b5f6cbaf51c6d17a03a91a95af337b4e5adc24 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 023/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9a8759da5cbadaf97e9c882d317145bba10dec6b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 024/197] Refactor Authenticator to interface and initialize one with provided factory. --- packages/opal-common/opal_common/config.py | 14 +++++++++++--- .../fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 449daceecb4a1834bfc9c1cccea4e5377624a629 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 025/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 7b9681b45bd931ca4ba4879f497058b7aa44f1b5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 026/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3db635b60a6758b8a76733eaf243d48981bcc004 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 027/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2ca7a4e12558b8628895032b503698b72a8533cc Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 028/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 90ec9d36ee8412cf16ee22fc8e3d79810ec8e37d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 029/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8fda746579fdb81263721a88645dfe941b32947b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 030/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2445b73c0990d42ef70e2f2182321e1fb0f0a15a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 031/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 396dd0916e1fc224d406dbd8b7469e38104c7c97 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 032/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5687c512ba6515a5d4824be37f6d23c33c15a96a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 033/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From adf1959540f75216d52d4815ac74ec2dcacefd63 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 034/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8ddc34fbe2344e70b7ca71d93c9c4856ff1d6048 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 035/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 1259c0f334d630490959f741b193a0222ad7c529 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 036/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0b0e18e4cbb9c36c1c9434caec0412e2c8df6173 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 037/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From dbe2c6bc2b8de766e768964d60cbeed665f26563 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 038/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6c6d85561e435761f91ce4dfdd93290721a01b01 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 039/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 38f7ee48f2d451bfd044009b37429284bc92a33c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 040/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From bc04703b847f33cd62d754bd3ce9373fb34b6662 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 041/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0d176696de093a9887e0100189a167230e921b99 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 042/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From cc6ea2e67ecf621585c2e1ba1a1e65c9f7aecd06 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 043/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 492115dabc0da57a05dec27cbdc666d0874cc2c5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 044/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3df3a207636ade72367e56bb6c02d5c13aecdaab Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 045/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 316188d8cc4352f31139e175dc268cb52a2ae1d6 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 046/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d1eb15e52d9af0227789e1f921150344c585617d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 047/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 419fb3d334a009edd3418c97a9b1359f25f73d26 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 048/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5b8c6e8a37e34bbb5d035c207e6778c31ddbc129 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 049/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 1cfe893c2ea12ac4d04eeeb14650fb5a3fea6684 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 050/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5064bbce75f1f30cfc801ce0cf1cfa39f2a1ebac Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 051/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 649cc895278b2291dd67478d8a6705dbd419d413 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 052/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8c341f93ab30f0f60af117eb0b7890d507774949 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 053/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 51fe585e0c9f39558482cf12d5dc36baec12b185 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 054/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9a0bb869d4bdfa5f40215cabc4e0a400e63009cf Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 055/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d05688b1836e4372e0cda95d4e120ce75fe00aa0 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 056/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8860e0a49f8330ea1848369e62ce232d597fae5e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 057/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 28014a97b7c0e24041392bf91d953dfc92fc89d5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 058/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 67c946ee7efca24e892f8ac01dd1cc6c8e7e72dc Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 059/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3f0d9883089a459e92220aa451a007953121537f Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 060/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From c8777bc9a7939d7271836b6e3f4ae420376a9e07 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 061/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9b94120b1366b982018f4b63a055c6a851bd48ba Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 062/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 01d7e921db148ef2d795ac2adf6902ab13f6eb31 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 063/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 30cfd1544a1c879d811fb53e38cf99c142840753 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 064/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b25aff4c0798ffd6280f2f65a09648bb4cb8d1f0 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 065/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e53dc1c940d784ba89c06445e1507c2d11045f31 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 066/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e276b8e6a1585ed99a2c5faa30d0125d1655e2d8 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 067/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9d821b406121ee785197042a8b026b2aebb2e94b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 068/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6775e604c7770190983900dc4d2ccf3252452bcb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 069/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From da3136ca83d49bb9e7484544f75c6e40b7b13692 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 070/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 12cf461d80a48e2ca77bbb4cea29ab8d65b1a503 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 071/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 29aa06a3c802fc74937ed2e71ab32eab572a0bcb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 072/197] Refactor Authenticator to interface and initialize one with provided factory. --- packages/opal-common/opal_common/config.py | 14 +++++++++++--- .../fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e9b6549c0f0aaf47485c9fa4bcedc339511211dd Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 073/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From dfcbb707031ddc34fcbf1fca1b79b3f3290a95c2 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 074/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b6e970ebbc3b2801b472a626ef4fd3adabb11194 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 075/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d6a87931b9a80996367865ee401ac96f532ea394 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 076/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3300ef504f247dc35a60e98a93234cd8fd72cfeb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 077/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 63d14fbd37eb8e571bded15d867fee76181692b2 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 078/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 36bcbc36f5c782ab8029e37ae8c5317e60bf5df9 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 079/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 130d6474f0d6c981e7780d6231d1364c0d748d54 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 080/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0c2496a28db54c5c91df18fe76ffa26647ef6e51 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 081/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From fc1e0d8f4f2bf8445799fa41479e827f60505fc3 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 082/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index ca2b5a1e1..5ccd511a6 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,17 +224,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From a22ce3da98ba2e2b368bf8d21457aac163d3e912 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 083/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 5ccd511a6..ca2b5a1e1 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -224,9 +224,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 50a08b2b4ef24333cb1738b8190b8a4117c778d4 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 084/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e5d4945cb8275da50a5b99f89ab9f5071a3e269c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 085/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6b871cf0a959bd1dc78c43e0aae389a01d67c70c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 086/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9a6bbac0613015e3b88dbd963faefcc3b323c1d6 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 087/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 328a3e45ce5b5a32e5a0e42a8614415c21a51561 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 088/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 581b7908ada57b1265e5790feb49b26c26c8aa5c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 089/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From a0bf603dc5c15fa517675226e9ba616ffb6affac Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 090/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From db0dbbd59280b4e3220e99406421e733ebb91bcf Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 091/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5b2cde8685db4722584bf40c8ba4db2df96d8159 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 092/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2edf4cdd56dde96203d772db02e7d9554528d4bf Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 093/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d4c556d82d1f72914eecfc775bb837cd1a8ae141 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 094/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d290274dbc539dde86a1415e39b46b5d9ddb3319 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 095/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 0ca9354c0..038c6c4c1 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 73ceea439dd11c98b69a0659a8e4dc497590ca2e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 096/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 038c6c4c1..0ca9354c0 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2f7d4ffcfac500db2e42adbebebb853a5b88b1af Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 097/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_client/data/oauth2_updater.py | 27 ++++++++++++----- .../opal-client/opal_client/data/updater.py | 7 +++-- .../opal_client/tests/data_updater_test.py | 7 ++++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- packages/requires.txt | 1 + 8 files changed, 60 insertions(+), 31 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-client/opal_client/data/oauth2_updater.py b/packages/opal-client/opal_client/data/oauth2_updater.py index 12adfd47a..55e6aa433 100644 --- a/packages/opal-client/opal_client/data/oauth2_updater.py +++ b/packages/opal-client/opal_client/data/oauth2_updater.py @@ -1,35 +1,46 @@ +from urllib.parse import parse_qs, urlencode, urlparse + import aiohttp from aiohttp.client import ClientSession from opal_client.logger import logger -from urllib.parse import urlencode, urlparse, parse_qs from .updater import DefaultDataUpdater class OAuth2DataUpdater(DefaultDataUpdater): - async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async def _load_policy_data_config( + self, url: str, headers + ) -> aiohttp.ClientResponse: await self._authenticator.authenticate(headers) async with ClientSession(headers=headers) as session: - response = await session.get(url, **self._ssl_context_kwargs, allow_redirects=False) + response = await session.get( + url, **self._ssl_context_kwargs, allow_redirects=False + ) if response.status == 307: - return await self._load_redirected_policy_data_config(response.headers['location'], headers) + return await self._load_redirected_policy_data_config( + response.headers["location"], headers + ) else: return response async def _load_redirected_policy_data_config(self, url: str, headers): redirect_url = self.__redirect_url(url) - logger.info("Redirecting to data-sources configuration '{source}'", source=redirect_url) + logger.info( + "Redirecting to data-sources configuration '{source}'", source=redirect_url + ) async with ClientSession(headers=headers) as session: - return await session.get(redirect_url, **self._ssl_context_kwargs, allow_redirects=False) + return await session.get( + redirect_url, **self._ssl_context_kwargs, allow_redirects=False + ) def __redirect_url(self, url: str) -> str: u = urlparse(url) query = parse_qs(u.query, keep_blank_values=True) - query.pop('token', None) + query.pop("token", None) u = u._replace(query=urlencode(query, True)) - return u.geturl() \ No newline at end of file + return u.geturl() diff --git a/packages/opal-client/opal_client/data/updater.py b/packages/opal-client/opal_client/data/updater.py index 27c590ccd..c16115bff 100644 --- a/packages/opal-client/opal_client/data/updater.py +++ b/packages/opal-client/opal_client/data/updater.py @@ -236,11 +236,10 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: url = self._data_sources_config_url logger.info("Getting data-sources configuration from '{source}'", source=url) - headers = {} if self._extra_headers is not None: headers = self._extra_headers.copy() - headers['Accept'] = "application/json" + headers["Accept"] = "application/json" try: response = await self._load_policy_data_config(url, headers) @@ -256,7 +255,9 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: logger.exception("Failed to load data sources config") raise - async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async def _load_policy_data_config( + self, url: str, headers + ) -> aiohttp.ClientResponse: async with ClientSession(headers=headers) as session: return await session.get(url, **self._ssl_context_kwargs) diff --git a/packages/opal-client/opal_client/tests/data_updater_test.py b/packages/opal-client/opal_client/tests/data_updater_test.py index eccd82a97..df342a308 100644 --- a/packages/opal-client/opal_client/tests/data_updater_test.py +++ b/packages/opal-client/opal_client/tests/data_updater_test.py @@ -21,7 +21,12 @@ from opal_client.config import opal_client_config from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater, DefaultDataUpdater +from opal_client.data.updater import ( + DataSourceEntry, + DataUpdate, + DataUpdater, + DefaultDataUpdater, +) from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/requires.txt b/packages/requires.txt index 84d128f45..6e99bc663 100644 --- a/packages/requires.txt +++ b/packages/requires.txt @@ -13,3 +13,4 @@ fastapi-utils>=0.2.1,<1 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability starlette>=0.40.0 # not directly required, pinned by Snyk to avoid a vulnerability +tls-cert-refresh-period From 78b0d7bbe11210328e27a6fd74a3897ad45fd08d Mon Sep 17 00:00:00 2001 From: Dan Yishai Date: Thu, 17 Jul 2025 02:11:59 +0300 Subject: [PATCH 098/197] Add trust_env for ClientSession to support proxy connections --- packages/opal-client/opal_client/data/oauth2_updater.py | 4 ++-- packages/opal-client/opal_client/data/updater.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/opal-client/opal_client/data/oauth2_updater.py b/packages/opal-client/opal_client/data/oauth2_updater.py index 55e6aa433..3677e98a4 100644 --- a/packages/opal-client/opal_client/data/oauth2_updater.py +++ b/packages/opal-client/opal_client/data/oauth2_updater.py @@ -13,7 +13,7 @@ async def _load_policy_data_config( ) -> aiohttp.ClientResponse: await self._authenticator.authenticate(headers) - async with ClientSession(headers=headers) as session: + async with ClientSession(headers=headers, trust_env=True) as session: response = await session.get( url, **self._ssl_context_kwargs, allow_redirects=False ) @@ -32,7 +32,7 @@ async def _load_redirected_policy_data_config(self, url: str, headers): "Redirecting to data-sources configuration '{source}'", source=redirect_url ) - async with ClientSession(headers=headers) as session: + async with ClientSession(headers=headers, trust_env=True) as session: return await session.get( redirect_url, **self._ssl_context_kwargs, allow_redirects=False ) diff --git a/packages/opal-client/opal_client/data/updater.py b/packages/opal-client/opal_client/data/updater.py index c16115bff..1f7fbe8f9 100644 --- a/packages/opal-client/opal_client/data/updater.py +++ b/packages/opal-client/opal_client/data/updater.py @@ -258,7 +258,7 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: async def _load_policy_data_config( self, url: str, headers ) -> aiohttp.ClientResponse: - async with ClientSession(headers=headers) as session: + async with ClientSession(headers=headers, trust_env=True) as session: return await session.get(url, **self._ssl_context_kwargs) async def get_base_policy_data( From 3a7919b810163d4355359e17a7ada46a380541bf Mon Sep 17 00:00:00 2001 From: Dan Yishai Date: Thu, 17 Jul 2025 02:14:02 +0300 Subject: [PATCH 099/197] Remove unused tls-cert-refresh-period from requirements --- packages/requires.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/requires.txt b/packages/requires.txt index 6e99bc663..84d128f45 100644 --- a/packages/requires.txt +++ b/packages/requires.txt @@ -13,4 +13,3 @@ fastapi-utils>=0.2.1,<1 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability starlette>=0.40.0 # not directly required, pinned by Snyk to avoid a vulnerability -tls-cert-refresh-period From a03c2ed70fc8c81d798fdd79be80a96ea156db41 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 100/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/config.py # packages/opal-server/opal_server/config.py # packages/requires.txt # Conflicts: # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/updater.py # Conflicts: # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py --- .../docker-compose-with-oauth-jwt-token.yml | 93 ++++++++++ ...docker-compose-with-oauth-opaque-token.yml | 83 +++++++++ .../opal-client/opal_client/callbacks/api.py | 4 +- packages/opal-client/opal_client/client.py | 37 ++-- .../opal_client/data/oauth2_updater.py | 35 ++++ .../opal-client/opal_client/data/updater.py | 46 +++-- .../opal_client/data/updater_factory.py | 70 ++++++++ .../opal-client/opal_client/policy/fetcher.py | 24 ++- .../opal-client/opal_client/policy/updater.py | 15 +- .../opal_client/policy_store/api.py | 4 +- .../opal_client/tests/data_updater_test.py | 8 +- .../server_to_client_intergation_test.py | 4 +- .../authentication/authenticator.py | 15 ++ .../authentication/authenticator_factory.py | 34 ++++ .../opal_common/authentication/authz.py | 6 +- .../opal_common/authentication/deps.py | 10 +- .../opal_common/authentication/jwk.py | 45 +++++ .../opal_common/authentication/oauth2.py | 164 ++++++++++++++++++ packages/opal-common/opal_common/config.py | 62 +++++++ .../fetcher/providers/http_fetch_provider.py | 14 +- .../opal_server/authentication/__init__.py | 0 .../authentication/authenticator.py | 18 ++ .../authentication/authenticator_factory.py | 49 ++++++ packages/opal-server/opal_server/data/api.py | 5 +- .../opal_server/policy/webhook/api.py | 4 +- packages/opal-server/opal_server/pubsub.py | 10 +- .../opal-server/opal_server/scopes/api.py | 6 +- .../opal-server/opal_server/security/jwks.py | 5 +- packages/opal-server/opal_server/server.py | 65 +++---- 29 files changed, 835 insertions(+), 100 deletions(-) create mode 100644 docker/docker-compose-with-oauth-jwt-token.yml create mode 100644 docker/docker-compose-with-oauth-opaque-token.yml create mode 100644 packages/opal-client/opal_client/data/oauth2_updater.py create mode 100644 packages/opal-client/opal_client/data/updater_factory.py create mode 100644 packages/opal-common/opal_common/authentication/authenticator.py create mode 100644 packages/opal-common/opal_common/authentication/authenticator_factory.py create mode 100644 packages/opal-common/opal_common/authentication/jwk.py create mode 100644 packages/opal-common/opal_common/authentication/oauth2.py create mode 100644 packages/opal-server/opal_server/authentication/__init__.py create mode 100644 packages/opal-server/opal_server/authentication/authenticator.py create mode 100644 packages/opal-server/opal_server/authentication/authenticator_factory.py diff --git a/docker/docker-compose-with-oauth-jwt-token.yml b/docker/docker-compose-with-oauth-jwt-token.yml new file mode 100644 index 000000000..b62197241 --- /dev/null +++ b/docker/docker-compose-with-oauth-jwt-token.yml @@ -0,0 +1,93 @@ +services: + # When scaling the opal-server to multiple nodes and/or multiple workers, we use + # a *broadcast* channel to sync between all the instances of opal-server. + # Under the hood, this channel is implemented by encode/broadcaster (see link below). + # At the moment, the broadcast channel can be either: postgresdb, redis or kafka. + # The format of the broadcaster URI string (the one we pass to opal server as `OPAL_BROADCAST_URI`) is specified here: + # https://github.com/encode/broadcaster#available-backends + broadcast_channel: + image: postgres:alpine + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + opal_server: + # by default we run opal-server from latest official image + image: permitio/opal-server:latest + environment: + # the broadcast backbone uri used by opal server workers (see comments above for: broadcast_channel) + - OPAL_BROADCAST_URI=postgres://postgres:postgres@broadcast_channel:5432/postgres + # number of uvicorn workers to run inside the opal-server container + - UVICORN_NUM_WORKERS=4 + # the git repo hosting our policy + # - if this repo is not public, you can pass an ssh key via `OPAL_POLICY_REPO_SSH_KEY`) + # - the repo we pass in this example is *public* and acts as an example repo with dummy rego policy + # - for more info, see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo + # in this example we will use a polling interval of 30 seconds to check for new policy updates (git commits affecting the rego policy). + # however, it is better to utilize a git *webhook* to trigger the server to check for changes only when the repo has new commits. + # for more info see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_POLLING_INTERVAL=30 + # configures from where the opal client should initially fetch data (when it first goes up, after disconnection, etc). + # the data sources represents from where the opal clients should get a "complete picture" of the data they need. + # after the initial sources are fetched, the client will subscribe only to update notifications sent by the server. + - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}} + - OPAL_LOG_FORMAT_INCLUDE_PID=true + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # JWT validation + - OPAL_OAUTH2_OPENID_CONFIGURATION_URL=https://example/.well-known/openid-configuration + - OPAL_OAUTH2_EXACT_MATCH_CLAIMS=aud=some_audience,iss=some_issuer + - OPAL_OAUTH2_REQUIRED_CLAIMS=sub,iat,exp + - OPAL_OAUTH2_JWT_ALGORITHM=RS256 + - OPAL_OAUTH2_JWT_AUDIENCE=some_audience + - OPAL_OAUTH2_JWT_ISSUER=https://example/issuer + ports: + # exposes opal server on the host machine, you can access the server at: http://localhost:7002 + - "7002:7002" + depends_on: + - broadcast_channel + opal_client: + # by default we run opal-client from latest official image + image: permitio/opal-client:latest + environment: + - OPAL_SERVER_URL=http://opal_server:7002 + - OPAL_LOG_FORMAT_INCLUDE_PID=true + - OPAL_INLINE_OPA_LOG_FORMAT=http + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # client credentials + - OPAL_OAUTH2_CLIENT_ID=some_client_id + - OPAL_OAUTH2_CLIENT_SECRET=some_client_secret + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # JWT validation + - OPAL_OAUTH2_OPENID_CONFIGURATION_URL=https://example/.well-known/openid-configuration + - OPAL_OAUTH2_EXACT_MATCH_CLAIMS=aud=some_audience,iss=some_issuer + - OPAL_OAUTH2_REQUIRED_CLAIMS=sub,iat,exp + - OPAL_OAUTH2_JWT_ALGORITHM=RS256 + - OPAL_OAUTH2_JWT_AUDIENCE=some_audience + - OPAL_OAUTH2_JWT_ISSUER=https://example/issuer + # Enable Authorization / Authentication in OPA + - 'OPAL_INLINE_OPA_CONFIG={"authentication":"token", "authorization":"basic", "files": ["authz.rego"]}' + volumes: + # The goal is to create an initial authorization rego that allows OPAL to write the first policy from the POLICY_REPO_URL. + # This is achieved through policy overwrite based on the "id" attribute. + # When the authz.rego file is placed in the root directory of OPA, it is given the id 'authz.rego'. + # Similarly, if there is another authz.rego file in the root of POLICY_REPO_URL, it will also be given the id 'authz.rego'. + # Therefore, if the authz.rego file from the POLICY_REPO_URL exists, it will overwrite the initial authz.rego file. + - ./docker_files/policy_test/authz.rego:/opal/authz.rego + ports: + # exposes opal client on the host machine, you can access the client at: http://localhost:7766 + - "7766:7000" + # exposes the OPA agent (being run by OPAL) on the host machine + # you can access the OPA api that you know and love at: http://localhost:8181 + # OPA api docs are at: https://www.openpolicyagent.org/docs/latest/rest-api/ + - "8181:8181" + depends_on: + - opal_server + # this command is not necessary when deploying OPAL for real, it is simply a trick for dev environments + # to make sure that opal-server is already up before starting the client. + command: sh -c "exec ./wait-for.sh opal_server:7002 --timeout=20 -- ./start.sh" diff --git a/docker/docker-compose-with-oauth-opaque-token.yml b/docker/docker-compose-with-oauth-opaque-token.yml new file mode 100644 index 000000000..7641cd0e8 --- /dev/null +++ b/docker/docker-compose-with-oauth-opaque-token.yml @@ -0,0 +1,83 @@ +services: + # When scaling the opal-server to multiple nodes and/or multiple workers, we use + # a *broadcast* channel to sync between all the instances of opal-server. + # Under the hood, this channel is implemented by encode/broadcaster (see link below). + # At the moment, the broadcast channel can be either: postgresdb, redis or kafka. + # The format of the broadcaster URI string (the one we pass to opal server as `OPAL_BROADCAST_URI`) is specified here: + # https://github.com/encode/broadcaster#available-backends + broadcast_channel: + image: postgres:alpine + environment: + - POSTGRES_DB=postgres + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + opal_server: + # by default we run opal-server from latest official image + image: permitio/opal-server:latest + environment: + # the broadcast backbone uri used by opal server workers (see comments above for: broadcast_channel) + - OPAL_BROADCAST_URI=postgres://postgres:postgres@broadcast_channel:5432/postgres + # number of uvicorn workers to run inside the opal-server container + - UVICORN_NUM_WORKERS=4 + # the git repo hosting our policy + # - if this repo is not public, you can pass an ssh key via `OPAL_POLICY_REPO_SSH_KEY`) + # - the repo we pass in this example is *public* and acts as an example repo with dummy rego policy + # - for more info, see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_URL=https://github.com/permitio/opal-example-policy-repo + # in this example we will use a polling interval of 30 seconds to check for new policy updates (git commits affecting the rego policy). + # however, it is better to utilize a git *webhook* to trigger the server to check for changes only when the repo has new commits. + # for more info see: https://docs.opal.ac/tutorials/track_a_git_repo + - OPAL_POLICY_REPO_POLLING_INTERVAL=30 + # configures from where the opal client should initially fetch data (when it first goes up, after disconnection, etc). + # the data sources represents from where the opal clients should get a "complete picture" of the data they need. + # after the initial sources are fetched, the client will subscribe only to update notifications sent by the server. + - OPAL_DATA_CONFIG_SOURCES={"config":{"entries":[{"url":"http://opal_server:7002/policy-data","topics":["policy_data"],"dst_path":"/static"}]}} + - OPAL_LOG_FORMAT_INCLUDE_PID=true + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # introspect URL for Opaque token validation + - OPAL_OAUTH2_INTROSPECT_URL=https://example/oauth2/introspect + ports: + # exposes opal server on the host machine, you can access the server at: http://localhost:7002 + - "7002:7002" + depends_on: + - broadcast_channel + opal_client: + # by default we run opal-client from latest official image + image: permitio/opal-client:latest + environment: + - OPAL_SERVER_URL=http://opal_server:7002 + - OPAL_LOG_FORMAT_INCLUDE_PID=true + - OPAL_INLINE_OPA_LOG_FORMAT=http + # to protect resources with OAuth2 Opaque token provided by dedicated server + - OPAL_AUTH_TYPE=oauth2 + # client credentials + - OPAL_OAUTH2_CLIENT_ID=some_client_id + - OPAL_OAUTH2_CLIENT_SECRET=some_client_secret + # URL to generate new OAuth 2.0 Client Credentials Grant token + - OPAL_OAUTH2_TOKEN_URL=https://example/oauth2/token + # introspect URL for Opaque token validation + - OPAL_OAUTH2_INTROSPECT_URL=https://example/oauth2/introspect + # Enable Authorization / Authentication in OPA + - 'OPAL_INLINE_OPA_CONFIG={"authentication":"token", "authorization":"basic", "files": ["authz.rego"]}' + volumes: + # The goal is to create an initial authorization rego that allows OPAL to write the first policy from the POLICY_REPO_URL. + # This is achieved through policy overwrite based on the "id" attribute. + # When the authz.rego file is placed in the root directory of OPA, it is given the id 'authz.rego'. + # Similarly, if there is another authz.rego file in the root of POLICY_REPO_URL, it will also be given the id 'authz.rego'. + # Therefore, if the authz.rego file from the POLICY_REPO_URL exists, it will overwrite the initial authz.rego file. + - ./docker_files/policy_test/authz.rego:/opal/authz.rego + ports: + # exposes opal client on the host machine, you can access the client at: http://localhost:7766 + - "7766:7000" + # exposes the OPA agent (being run by OPAL) on the host machine + # you can access the OPA api that you know and love at: http://localhost:8181 + # OPA api docs are at: https://www.openpolicyagent.org/docs/latest/rest-api/ + - "8181:8181" + depends_on: + - opal_server + # this command is not necessary when deploying OPAL for real, it is simply a trick for dev environments + # to make sure that opal-server is already up before starting the client. + command: sh -c "exec ./wait-for.sh opal_server:7002 --timeout=20 -- ./start.sh" diff --git a/packages/opal-client/opal_client/callbacks/api.py b/packages/opal-client/opal_client/callbacks/api.py index a2e2d5a63..90b1e6ecd 100644 --- a/packages/opal-client/opal_client/callbacks/api.py +++ b/packages/opal-client/opal_client/callbacks/api.py @@ -3,8 +3,8 @@ from fastapi import APIRouter, Depends, HTTPException, Response, status from opal_client.callbacks.register import CallbacksRegister from opal_client.config import opal_client_config +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import require_peer_type -from opal_common.authentication.deps import JWTAuthenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -13,7 +13,7 @@ from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR -def init_callbacks_api(authenticator: JWTAuthenticator, register: CallbacksRegister): +def init_callbacks_api(authenticator: Authenticator, register: CallbacksRegister): async def require_listener_token(claims: JWTClaims = Depends(authenticator)): try: require_peer_type( diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 7944f65d8..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -2,9 +2,7 @@ import functools import os import signal -import tempfile import uuid -from logging import disable from typing import Awaitable, Callable, List, Literal, Optional, Union import aiofiles @@ -19,8 +17,8 @@ from opal_client.callbacks.register import CallbacksRegister from opal_client.config import PolicyStoreTypes, opal_client_config from opal_client.data.api import init_data_router -from opal_client.data.fetcher import DataFetcher from opal_client.data.updater import DataUpdater +from opal_client.data.updater_factory import DataUpdaterFactory from opal_client.engine.options import CedarServerOptions, OpaServerOptions from opal_client.engine.runner import CedarRunner, OpaRunner from opal_client.limiter import StartupLoadLimiter @@ -31,8 +29,8 @@ from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) -from opal_common.authentication.deps import JWTAuthenticator -from opal_common.authentication.verifier import JWTVerifier +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.logger import configure_logs, logger from opal_common.middleware import configure_middleware @@ -51,7 +49,7 @@ def __init__( inline_opa_options: OpaServerOptions = None, inline_cedar_enabled: bool = None, inline_cedar_options: CedarServerOptions = None, - verifier: Optional[JWTVerifier] = None, + authenticator: Optional[Authenticator] = None, store_backup_path: Optional[str] = None, store_backup_interval: Optional[int] = None, offline_mode_enabled: bool = False, @@ -70,6 +68,10 @@ def __init__( data_updater (DataUpdater, optional): Defaults to None. policy_updater (PolicyUpdater, optional): Defaults to None. """ + if authenticator is not None: + self.authenticator = authenticator + else: + self.authenticator = AuthenticatorFactory.create() self._shard_id = shard_id # defaults policy_store_type: PolicyStoreTypes = ( @@ -127,6 +129,7 @@ def __init__( opal_client_id=opal_client_identifier, on_connect=on_policy_updater_connect, on_disconnect=on_policy_updater_disconnect, + authenticator=self.authenticator, ) else: self.policy_updater = None @@ -142,7 +145,7 @@ def __init__( else opal_client_config.DATA_TOPICS ) - self.data_updater = DataUpdater( + self.data_updater = DataUpdaterFactory.create( policy_store=self.policy_store, data_topics=data_topics, callbacks_register=self._callbacks_register, @@ -150,6 +153,7 @@ def __init__( shard_id=self._shard_id, on_connect=on_data_updater_connect, on_disconnect=on_data_updater_disconnect, + authenticator=self.authenticator, ) else: self.data_updater = None @@ -172,19 +176,6 @@ def __init__( "OPAL client is configured to trust self-signed certificates" ) - if verifier is not None: - self.verifier = verifier - else: - self.verifier = JWTVerifier( - public_key=opal_common_config.AUTH_PUBLIC_KEY, - algorithm=opal_common_config.AUTH_JWT_ALGORITHM, - audience=opal_common_config.AUTH_JWT_AUDIENCE, - issuer=opal_common_config.AUTH_JWT_ISSUER, - ) - if not self.verifier.enabled: - logger.info( - "API authentication disabled (public encryption key was not provided)" - ) self.store_backup_path = ( store_backup_path or opal_client_config.STORE_BACKUP_PATH ) @@ -264,13 +255,11 @@ async def _is_ready(self): def _configure_api_routes(self, app: FastAPI): """Mounts the api routes on the app object.""" - authenticator = JWTAuthenticator(self.verifier) - # Init api routers with required dependencies policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) - policy_store_router = init_policy_store_router(authenticator) - callbacks_router = init_callbacks_api(authenticator, self._callbacks_register) + policy_store_router = init_policy_store_router(self.authenticator) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-client/opal_client/data/oauth2_updater.py b/packages/opal-client/opal_client/data/oauth2_updater.py new file mode 100644 index 000000000..12adfd47a --- /dev/null +++ b/packages/opal-client/opal_client/data/oauth2_updater.py @@ -0,0 +1,35 @@ +import aiohttp +from aiohttp.client import ClientSession +from opal_client.logger import logger +from urllib.parse import urlencode, urlparse, parse_qs + +from .updater import DefaultDataUpdater + + +class OAuth2DataUpdater(DefaultDataUpdater): + async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + await self._authenticator.authenticate(headers) + + async with ClientSession(headers=headers) as session: + response = await session.get(url, **self._ssl_context_kwargs, allow_redirects=False) + + if response.status == 307: + return await self._load_redirected_policy_data_config(response.headers['location'], headers) + else: + return response + + async def _load_redirected_policy_data_config(self, url: str, headers): + redirect_url = self.__redirect_url(url) + + logger.info("Redirecting to data-sources configuration '{source}'", source=redirect_url) + + async with ClientSession(headers=headers) as session: + return await session.get(redirect_url, **self._ssl_context_kwargs, allow_redirects=False) + + def __redirect_url(self, url: str) -> str: + u = urlparse(url) + query = parse_qs(u.query, keep_blank_values=True) + query.pop('token', None) + u = u._replace(query=urlencode(query, True)) + + return u.geturl() \ No newline at end of file diff --git a/packages/opal-client/opal_client/data/updater.py b/packages/opal-client/opal_client/data/updater.py index eadc9f0da..9cd6503ce 100644 --- a/packages/opal-client/opal_client/data/updater.py +++ b/packages/opal-client/opal_client/data/updater.py @@ -3,7 +3,7 @@ import json import uuid from functools import partial -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import aiohttp from aiohttp.client import ClientError, ClientSession @@ -25,6 +25,8 @@ DEFAULT_POLICY_STORE_GETTER, ) from opal_common.async_utils import TasksPool, repeated_call +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.http_utils import is_http_error_response from opal_common.schemas.data import ( @@ -71,6 +73,7 @@ def __init__( shard_id: Optional[str] = None, on_connect: List[PubSubOnConnectCallback] = None, on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, ): """Initializes the DataUpdater with the necessary configuration and clients. @@ -165,6 +168,10 @@ def __init__( # Optional user-defined hooks for connection lifecycle self._on_connect_callbacks = on_connect or [] self._on_disconnect_callbacks = on_disconnect or [] + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() async def __aenter__(self): await self.start() @@ -229,22 +236,30 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: url = self._data_sources_config_url logger.info("Getting data-sources configuration from '{source}'", source=url) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + headers['Accept'] = "application/json" + try: - async with ClientSession( - headers=self._extra_headers, trust_env=True - ) as session: - response = await session.get(url, **self._ssl_context_kwargs) - if response.status == 200: - return DataSourceConfig.parse_obj(await response.json()) - else: - error_details = await response.json() - raise ClientError( - f"Fetch data sources failed with status code {response.status}, error: {error_details}" - ) + response = await self._load_policy_data_config(url, headers) + + if response.status == 200: + return DataSourceConfig.parse_obj(await response.json()) + else: + error_details = await response.text() + raise ClientError( + f"Fetch data sources failed with status code {response.status}, error: {error_details}" + ) except: logger.exception("Failed to load data sources config") raise + async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async with ClientSession(headers=headers, trust_env=True) as session: + return await session.get(url, **self._ssl_context_kwargs) + async def get_base_policy_data( self, config_url: str = None, data_fetch_reason="Initial load" ): @@ -343,6 +358,12 @@ async def _subscriber(self): callback. """ logger.info("Subscribing to topics: {topics}", topics=self._data_topics) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + await self._authenticator.authenticate(headers) + self._client = PubSubClient( self._data_topics, self._update_policy_data_callback, @@ -350,6 +371,7 @@ async def _subscriber(self): on_connect=[self.on_connect, *self._on_connect_callbacks], on_disconnect=[self.on_disconnect, *self._on_disconnect_callbacks], additional_headers=self._extra_headers, + extra_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, diff --git a/packages/opal-client/opal_client/data/updater_factory.py b/packages/opal-client/opal_client/data/updater_factory.py new file mode 100644 index 000000000..ad784f996 --- /dev/null +++ b/packages/opal-client/opal_client/data/updater_factory.py @@ -0,0 +1,70 @@ +from typing import List, Optional + +from fastapi_websocket_pubsub.pub_sub_client import PubSubOnConnectCallback +from fastapi_websocket_rpc.rpc_channel import OnDisconnectCallback +from opal_client.callbacks.register import CallbacksRegister +from opal_client.data.fetcher import DataFetcher +from opal_client.policy_store.base_policy_store_client import BasePolicyStoreClient +from opal_common.authentication.authenticator import Authenticator +from opal_common.config import opal_common_config +from opal_common.logger import logger + +from .oauth2_updater import OAuth2DataUpdater +from .updater import DataUpdater, DefaultDataUpdater + + +class DataUpdaterFactory: + @staticmethod + def create( + token: str = None, + pubsub_url: str = None, + data_sources_config_url: str = None, + fetch_on_connect: bool = True, + data_topics: List[str] = None, + policy_store: BasePolicyStoreClient = None, + should_send_reports=None, + data_fetcher: Optional[DataFetcher] = None, + callbacks_register: Optional[CallbacksRegister] = None, + opal_client_id: str = None, + shard_id: Optional[str] = None, + on_connect: List[PubSubOnConnectCallback] = None, + on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, + ) -> DataUpdater: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will authenticate Datasource requests with OAuth2 tokens." + ) + return OAuth2DataUpdater( + token, + pubsub_url, + data_sources_config_url, + fetch_on_connect, + data_topics, + policy_store, + should_send_reports, + data_fetcher, + callbacks_register, + opal_client_id, + shard_id, + on_connect, + on_disconnect, + authenticator, + ) + else: + return DefaultDataUpdater( + token, + pubsub_url, + data_sources_config_url, + fetch_on_connect, + data_topics, + policy_store, + should_send_reports, + data_fetcher, + callbacks_register, + opal_client_id, + shard_id, + on_connect, + on_disconnect, + authenticator, + ) diff --git a/packages/opal-client/opal_client/policy/fetcher.py b/packages/opal-client/opal_client/policy/fetcher.py index be0de14ca..c309ed6ba 100644 --- a/packages/opal-client/opal_client/policy/fetcher.py +++ b/packages/opal-client/opal_client/policy/fetcher.py @@ -4,6 +4,8 @@ from fastapi import HTTPException, status from opal_client.config import opal_client_config from opal_client.logger import logger +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.schemas.policy import PolicyBundle from opal_common.security.sslcontext import get_custom_ssl_context from opal_common.utils import ( @@ -28,15 +30,27 @@ def force_valid_bundle(bundle) -> PolicyBundle: class PolicyFetcher: """Fetches policy from backend.""" - def __init__(self, backend_url=None, token=None): + def __init__( + self, + backend_url=None, + token=None, + authenticator: Optional[Authenticator] = None, + ): """ Args: backend_url (str): Defaults to opal_client_config.SERVER_URL. token ([type], optional): [description]. Defaults to opal_client_config.CLIENT_TOKEN. """ + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() self._token = token or opal_client_config.CLIENT_TOKEN self._backend_url = backend_url or opal_client_config.SERVER_URL - self._auth_headers = tuple_to_dict(get_authorization_header(self._token)) + if self._token is not None: + self._auth_headers = tuple_to_dict(get_authorization_header(self._token)) + else: + self._auth_headers = dict() self._retry_config = ( opal_client_config.POLICY_UPDATER_CONN_RETRY.toTenacityConfig() @@ -82,10 +96,16 @@ async def _fetch_policy_bundle( May throw, in which case we retry again. """ + headers = {} + if self._auth_headers is not None: + headers = self._auth_headers.copy() + await self._authenticator.authenticate(headers) + params = {"path": directories} if base_hash is not None: params["base_hash"] = base_hash async with aiohttp.ClientSession( + headers=headers, trust_env=True, ) as session: logger.info( diff --git a/packages/opal-client/opal_client/policy/updater.py b/packages/opal-client/opal_client/policy/updater.py index b2e56f3d8..07c57c6a4 100644 --- a/packages/opal-client/opal_client/policy/updater.py +++ b/packages/opal-client/opal_client/policy/updater.py @@ -46,6 +46,7 @@ def __init__( opal_client_id: str = None, on_connect: List[PubSubOnConnectCallback] = None, on_disconnect: List[OnDisconnectCallback] = None, + authenticator: Optional[Authenticator] = None, ): """Inits the policy updater. @@ -67,6 +68,10 @@ def __init__( self._opal_client_id = opal_client_id self._scope_id = opal_client_config.SCOPE_ID + if authenticator is not None: + self._authenticator = authenticator + else: + self._authenticator = AuthenticatorFactory.create() # The policy store we'll save policy modules into (i.e: OPA) self._policy_store = policy_store or DEFAULT_POLICY_STORE_GETTER() # pub/sub server url and authentication data @@ -90,7 +95,7 @@ def __init__( self._policy_update_task = None self._stopping = False # policy fetcher - fetches policy bundles - self._policy_fetcher = PolicyFetcher() + self._policy_fetcher = PolicyFetcher(authenticator=self._authenticator) # callbacks on policy changes self._data_fetcher = data_fetcher or DataFetcher() self._callbacks_register = callbacks_register or CallbacksRegister() @@ -245,12 +250,18 @@ async def _subscriber(self): update_policy() callback (which will fetch the relevant policy bundle from the server and update the policy store).""" logger.info("Subscribing to topics: {topics}", topics=self._topics) + + headers = {} + if self._extra_headers is not None: + headers = self._extra_headers.copy() + await self._authenticator.authenticate(headers) + self._client = PubSubClient( topics=self._topics, callback=self._update_policy_callback, on_connect=[self._on_connect, *self._on_connect_callbacks], on_disconnect=[self._on_disconnect, *self._on_disconnect_callbacks], - additional_headers=self._extra_headers, + extra_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, diff --git a/packages/opal-client/opal_client/policy_store/api.py b/packages/opal-client/opal_client/policy_store/api.py index b27d83d70..97113f109 100644 --- a/packages/opal-client/opal_client/policy_store/api.py +++ b/packages/opal-client/opal_client/policy_store/api.py @@ -1,15 +1,15 @@ from fastapi import APIRouter, Depends from opal_client.config import opal_client_config from opal_client.policy_store.schemas import PolicyStoreAuth, PolicyStoreDetails +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import require_peer_type -from opal_common.authentication.deps import JWTAuthenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger from opal_common.schemas.security import PeerType -def init_policy_store_router(authenticator: JWTAuthenticator): +def init_policy_store_router(authenticator: Authenticator): router = APIRouter() @router.get( diff --git a/packages/opal-client/opal_client/tests/data_updater_test.py b/packages/opal-client/opal_client/tests/data_updater_test.py index cad760a10..eccd82a97 100644 --- a/packages/opal-client/opal_client/tests/data_updater_test.py +++ b/packages/opal-client/opal_client/tests/data_updater_test.py @@ -21,7 +21,7 @@ from opal_client.config import opal_client_config from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater +from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater, DefaultDataUpdater from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) @@ -171,7 +171,7 @@ async def test_data_updater(server): server trigger a Data-update and check our policy store gets the update.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, policy_store=policy_store, fetch_on_connect=False, @@ -251,7 +251,7 @@ async def test_data_updater_with_report_callback(server): server trigger a Data-update and check our policy store gets the update.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, policy_store=policy_store, fetch_on_connect=False, @@ -311,7 +311,7 @@ async def test_client_get_initial_data(server): """Connect to OPAL-server and make sure data is fetched on-connect.""" # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - updater = DataUpdater( + updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, data_sources_config_url=DATA_CONFIG_URL, policy_store=policy_store, diff --git a/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py b/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py index a3372c56f..5ce829fd7 100644 --- a/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py +++ b/packages/opal-client/opal_client/tests/server_to_client_intergation_test.py @@ -18,7 +18,7 @@ from opal_client import OpalClient from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater +from opal_client.data.updater import DataSourceEntry, DataUpdate, DefaultDataUpdater from opal_client.policy_store.mock_policy_store_client import MockPolicyStoreClient from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, @@ -76,7 +76,7 @@ async def startup_event(): def setup_client(event): # config to use mock OPA policy_store = PolicyStoreClientFactory.create(store_type=PolicyStoreTypes.MOCK) - data_updater = DataUpdater( + data_updater = DefaultDataUpdater( pubsub_url=UPDATES_URL, data_sources_config_url=DATA_CONFIG_URL, policy_store=policy_store, diff --git a/packages/opal-common/opal_common/authentication/authenticator.py b/packages/opal-common/opal_common/authentication/authenticator.py new file mode 100644 index 000000000..87e210ad4 --- /dev/null +++ b/packages/opal-common/opal_common/authentication/authenticator.py @@ -0,0 +1,15 @@ +from typing import Optional + +from opal_common.authentication.signer import JWTSigner + + +class Authenticator: + @property + def enabled(self) -> bool: + raise NotImplementedError() + + def signer(self) -> Optional[JWTSigner]: + raise NotImplementedError() + + async def authenticate(self, headers): + raise NotImplementedError() diff --git a/packages/opal-common/opal_common/authentication/authenticator_factory.py b/packages/opal-common/opal_common/authentication/authenticator_factory.py new file mode 100644 index 000000000..c0bcc40ad --- /dev/null +++ b/packages/opal-common/opal_common/authentication/authenticator_factory.py @@ -0,0 +1,34 @@ +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.verifier import JWTVerifier, Unauthorized +from opal_common.config import opal_common_config +from opal_common.logger import logger + +from .oauth2 import CachedOAuth2Authenticator, OAuth2ClientCredentialsAuthenticator + + +class AuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will authenticate API requests with OAuth2 tokens." + ) + return CachedOAuth2Authenticator(OAuth2ClientCredentialsAuthenticator()) + else: + return JWTAuthenticator(AuthenticatorFactory.__verifier()) + + @staticmethod + def __verifier() -> JWTVerifier: + verifier = JWTVerifier( + public_key=opal_common_config.AUTH_PUBLIC_KEY, + algorithm=opal_common_config.AUTH_JWT_ALGORITHM, + audience=opal_common_config.AUTH_JWT_AUDIENCE, + issuer=opal_common_config.AUTH_JWT_ISSUER, + ) + if not verifier.enabled: + logger.info( + "API authentication disabled (public encryption key was not provided)" + ) + + return verifier diff --git a/packages/opal-common/opal_common/authentication/authz.py b/packages/opal-common/opal_common/authentication/authz.py index 742304bf5..822497e64 100644 --- a/packages/opal-common/opal_common/authentication/authz.py +++ b/packages/opal-common/opal_common/authentication/authz.py @@ -1,4 +1,4 @@ -from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.schemas.data import DataUpdate @@ -6,7 +6,7 @@ def require_peer_type( - authenticator: JWTAuthenticator, claims: JWTClaims, required_type: PeerType + authenticator: Authenticator, claims: JWTClaims, required_type: PeerType ): if not authenticator.enabled: return @@ -28,7 +28,7 @@ def require_peer_type( def restrict_optional_topics_to_publish( - authenticator: JWTAuthenticator, claims: JWTClaims, update: DataUpdate + authenticator: Authenticator, claims: JWTClaims, update: DataUpdate ): if not authenticator.enabled: return diff --git a/packages/opal-common/opal_common/authentication/deps.py b/packages/opal-common/opal_common/authentication/deps.py index 2ec63043b..332a37cb7 100644 --- a/packages/opal-common/opal_common/authentication/deps.py +++ b/packages/opal-common/opal_common/authentication/deps.py @@ -4,6 +4,8 @@ from fastapi import Header from fastapi.exceptions import HTTPException from fastapi.security.utils import get_authorization_scheme_param +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.signer import JWTSigner from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.logger import logger @@ -67,7 +69,7 @@ def verify_logged_in(verifier: JWTVerifier, token: Optional[str]) -> JWTClaims: raise -class _JWTAuthenticator: +class _JWTAuthenticator(Authenticator): def __init__(self, verifier: JWTVerifier): self._verifier = verifier @@ -75,10 +77,16 @@ def __init__(self, verifier: JWTVerifier): def verifier(self) -> JWTVerifier: return self._verifier + def signer(self) -> Optional[JWTSigner]: + return self._verifier + @property def enabled(self) -> JWTVerifier: return self._verifier.enabled + async def authenticate(self, headers): + pass + class JWTAuthenticator(_JWTAuthenticator): """Bearer token authentication for http(s) api endpoints. diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py new file mode 100644 index 000000000..182b5cdb9 --- /dev/null +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -0,0 +1,45 @@ +import jwt +import httpx + +from cachetools import TTLCache +from opal_common.authentication.verifier import Unauthorized + +class JWKManager: + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + self._openid_configuration_url = openid_configuration_url + self._jwt_algorithm = jwt_algorithm + self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) + + def public_key(self, token): + header = jwt.get_unverified_header(token) + kid = header['kid'] + + public_key = self._cache.get(kid) + if public_key is None: + public_key = self._fetch_public_key(token) + self._cache[kid] = public_key + + return public_key + + def _fetch_public_key(self, token: str): + try: + return self._jwks_client().get_signing_key_from_jwt(token).key + except Exception: + raise Unauthorized(description="unknown JWT error") + + def _jwks_client(self): + oidc_config = self._openid_configuration() + signing_algorithms = oidc_config["id_token_signing_alg_values_supported"] + if self._jwt_algorithm.name not in signing_algorithms: + raise Unauthorized(description="unknown JWT algorithm") + if "jwks_uri" not in oidc_config: + raise Unauthorized(description="missing 'jwks_uri' property") + return jwt.PyJWKClient(oidc_config["jwks_uri"]) + + def _openid_configuration(self): + response = httpx.get(self._openid_configuration_url) + + if response.status_code != httpx.codes.OK: + raise Unauthorized(description=f"invalid status code {response.status_code}") + + return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py new file mode 100644 index 000000000..33bc4647a --- /dev/null +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -0,0 +1,164 @@ +import asyncio +import httpx +import time + +from cachetools import cached, TTLCache +from fastapi import Header +from httpx import AsyncClient, BasicAuth +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import get_token_from_header +from opal_common.authentication.jwk import JWKManager +from opal_common.authentication.signer import JWTSigner +from opal_common.authentication.verifier import JWTVerifier, Unauthorized +from opal_common.config import opal_common_config +from typing import Optional + +class _OAuth2Authenticator(Authenticator): + async def authenticate(self, headers): + if "Authorization" not in headers: + token = await self.token() + headers['Authorization'] = f"Bearer {token}" + + +class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): + def __init__(self) -> None: + self._client_id = opal_common_config.OAUTH2_CLIENT_ID + self._client_secret = opal_common_config.OAUTH2_CLIENT_SECRET + self._token_url = opal_common_config.OAUTH2_TOKEN_URL + self._introspect_url = opal_common_config.OAUTH2_INTROSPECT_URL + self._jwt_algorithm = opal_common_config.OAUTH2_JWT_ALGORITHM + self._jwt_audience = opal_common_config.OAUTH2_JWT_AUDIENCE + self._jwt_issuer = opal_common_config.OAUTH2_JWT_ISSUER + self._jwk_manager = JWKManager( + opal_common_config.OAUTH2_OPENID_CONFIGURATION_URL, + opal_common_config.OAUTH2_JWT_ALGORITHM, + opal_common_config.OAUTH2_JWK_CACHE_MAXSIZE, + opal_common_config.OAUTH2_JWK_CACHE_TTL, + ) + + cfg = opal_common_config.OAUTH2_EXACT_MATCH_CLAIMS + if cfg is None: + self._exact_match_claims = {} + else: + self._exact_match_claims = dict(map(lambda x: x.split("="), cfg.split(","))) + + cfg = opal_common_config.OAUTH2_REQUIRED_CLAIMS + if cfg is None: + self._required_claims = [] + else: + self._required_claims = cfg.split(",") + + @property + def enabled(self): + return True + + def signer(self) -> Optional[JWTSigner]: + return None + + async def token(self): + auth = BasicAuth(self._client_id, self._client_secret) + data = {"grant_type": "client_credentials"} + + async with AsyncClient() as client: + response = await client.post(self._token_url, auth=auth, data=data) + return (response.json())['access_token'] + + def __call__(self, authorization: Optional[str] = Header(None)) -> {}: + token = get_token_from_header(authorization) + return self.verify(token) + + def verify(self, token: str) -> {}: + if self._introspect_url is not None: + claims = self._verify_opaque(token) + else: + claims = self._verify_jwt(token) + + self._verify_exact_match_claims(claims) + self._verify_required_claims(claims) + + return claims + + def _verify_opaque(self, token: str) -> {}: + response = httpx.post(self._introspect_url, data={'token': token}) + + if response.status_code != httpx.codes.OK: + raise Unauthorized(description=f"invalid status code {response.status_code}") + + claims = response.json() + active = claims.get("active", False) + if not active: + raise Unauthorized(description="inactive token") + + return claims or {} + + def _verify_jwt(self, token: str) -> {}: + public_key = self._jwk_manager.public_key(token) + + verifier = JWTVerifier( + public_key=public_key, + algorithm=self._jwt_algorithm, + audience=self._jwt_audience, + issuer=self._jwt_issuer, + ) + claims = verifier.verify(token) + + return claims or {} + + def _verify_exact_match_claims(self, claims): + for key, value in self._exact_match_claims.items(): + if key not in claims: + raise Unauthorized(description=f"missing required '{key}' claim") + elif claims[key] != value: + raise Unauthorized(description=f"invalid '{key}' claim value") + + def _verify_required_claims(self, claims): + for claim in self._required_claims: + if claim not in claims: + raise Unauthorized(description=f"missing required '{claim}' claim") + + +class CachedOAuth2Authenticator(_OAuth2Authenticator): + lock = asyncio.Lock() + + def __init__(self, delegate: OAuth2ClientCredentialsAuthenticator) -> None: + self._token = None + self._exp = None + self._exp_margin = opal_common_config.OAUTH2_EXP_MARGIN + self._delegate = delegate + + @property + def enabled(self): + return self._delegate.enabled + + def signer(self) -> Optional[JWTSigner]: + return self._delegate.signer() + + def _expired(self): + if self._token is None: + return True + + now = int(time.time()) + return now > self._exp - self._exp_margin + + async def token(self): + if not self._expired(): + return self._token + + async with CachedOAuth2Authenticator.lock: + if not self._expired(): + return self._token + + token = await self._delegate.token() + claims = self._delegate.verify(token) + + self._token = token + self._exp = claims['exp'] + + return self._token + + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) + def __call__(self, authorization: Optional[str] = Header(None)) -> {}: + return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index a65dd6211..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -197,6 +197,68 @@ class OpalCommonConfig(Confi): [".rego"], description="List of extensions to serve as policy modules", ) + AUTH_TYPE = confi.str( + "AUTH_TYPE", + None, + description="Authentication type. Available options are oauth2 for validating access token via either OAUTH2_INTROSPECT_URL or OPAL_OAUTH2_OPENID_CONFIGURATION_URL or anything else if you prefer OPAL to do the job.", + ) + OAUTH2_CLIENT_ID = confi.str( + "OAUTH2_CLIENT_ID", None, description="OAuth2 Client ID." + ) + OAUTH2_CLIENT_SECRET = confi.str( + "OAUTH2_CLIENT_SECRET", None, description="OAuth2 Client Secret." + ) + OAUTH2_TOKEN_URL = confi.str( + "OAUTH2_TOKEN_URL", None, description="OAuth2 Token URL." + ) + OAUTH2_INTROSPECT_URL = confi.str( + "OAUTH2_INTROSPECT_URL", None, description="OAuth2 introspect URL." + ) + OAUTH2_OPENID_CONFIGURATION_URL = confi.str( + "OAUTH2_OPENID_CONFIGURATION_URL", + None, + description="OAuth2 OpenID configuration URL.", + ) + OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE = confi.int( + "OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE", + 100, + description="OAuth2 token validation cache maxsize.", + ) + OAUTH2_TOKEN_VERIFY_CACHE_TTL = confi.int( + "OAUTH2_TOKEN_VERIFY_CACHE_TTL", + 5 * 60, + description="OAuth2 token validation cache TTL.", + ) + + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) + OAUTH2_JWT_ALGORITHM = confi.enum( + "OAUTH2_JWT_ALGORITHM", + JWTAlgorithm, + getattr(JWTAlgorithm, "RS256"), + description="jwt algorithm, possible values: see: https://pyjwt.readthedocs.io/en/stable/algorithms.html", + ) + OAUTH2_JWT_AUDIENCE = confi.str( + "OAUTH2_JWT_AUDIENCE", None, description="OAuth2 required audience" + ) + OAUTH2_JWT_ISSUER = confi.str( + "OAUTH2_JWT_ISSUER", None, description="OAuth2 required issuer" + ) + OAUTH2_JWK_CACHE_MAXSIZE = confi.int( + "OAUTH2_JWK_CACHE_MAXSIZE", 100, description="OAuth2 JWKS cache maxsize." + ) + OAUTH2_JWK_CACHE_TTL = confi.int( + "OAUTH2_JWK_CACHE_TTL", 7 * 24 * 60 * 60, description="OAuth2 JWKS cache TTL." + ) ENABLE_METRICS = confi.bool( "ENABLE_METRICS", False, description="Enable metrics collection" diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 80ac50202..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -1,10 +1,12 @@ """Simple HTTP get data fetcher using requests supports.""" from enum import Enum -from typing import Any, Union, cast +from typing import Any, Optional, Union, cast import httpx from aiohttp import ClientResponse, ClientSession, ClientTimeout +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.fetcher.events import FetcherConfig, FetchEvent from opal_common.fetcher.fetch_provider import BaseFetchProvider @@ -52,6 +54,8 @@ class HttpFetchEvent(FetchEvent): class HttpFetchProvider(BaseFetchProvider): + _authenticator: Optional[Authenticator] = None + def __init__(self, event: HttpFetchEvent) -> None: self._event: HttpFetchEvent if event.config is None: @@ -64,6 +68,9 @@ def __init__(self, event: HttpFetchEvent) -> None: if self._custom_ssl_context is not None else {} ) + if HttpFetchProvider._authenticator is None: + HttpFetchProvider._authenticator = AuthenticatorFactory.create() + self._authenticator = HttpFetchProvider._authenticator def parse_event(self, event: FetchEvent) -> HttpFetchEvent: return HttpFetchEvent(**event.dict(exclude={"config"}), config=event.config) @@ -72,7 +79,10 @@ async def __aenter__(self): headers = {} timeout = opal_common_config.HTTP_FETCHER_TIMEOUT if self._event.config.headers is not None: - headers = self._event.config.headers + headers = self._event.config.headers.copy() + + await self._authenticator.authenticate(headers) + if opal_common_config.HTTP_FETCHER_PROVIDER_CLIENT == "httpx": self._session = httpx.AsyncClient( headers=headers, timeout=timeout, trust_env=True diff --git a/packages/opal-server/opal_server/authentication/__init__.py b/packages/opal-server/opal_server/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/opal-server/opal_server/authentication/authenticator.py b/packages/opal-server/opal_server/authentication/authenticator.py new file mode 100644 index 000000000..def646382 --- /dev/null +++ b/packages/opal-server/opal_server/authentication/authenticator.py @@ -0,0 +1,18 @@ +from typing import Optional + +from fastapi import Header +from fastapi.exceptions import HTTPException +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.types import JWTClaims +from opal_common.authentication.verifier import JWTVerifier, Unauthorized + + +class WebsocketServerAuthenticator(Authenticator): + def __init__(self, delegate: Authenticator) -> None: + self._delegate = delegate + + def __call__(self, authorization: Optional[str] = Header(None)) -> JWTClaims: + try: + return self._delegate(authorization) + except (Unauthorized, HTTPException): + return None diff --git a/packages/opal-server/opal_server/authentication/authenticator_factory.py b/packages/opal-server/opal_server/authentication/authenticator_factory.py new file mode 100644 index 000000000..43f4f092f --- /dev/null +++ b/packages/opal-server/opal_server/authentication/authenticator_factory.py @@ -0,0 +1,49 @@ +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.oauth2 import ( + CachedOAuth2Authenticator, + OAuth2ClientCredentialsAuthenticator, +) +from opal_common.authentication.signer import JWTSigner +from opal_common.config import opal_common_config +from opal_common.logger import logger +from opal_server.config import opal_server_config + +from .authenticator import WebsocketServerAuthenticator + + +class ServerAuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + if opal_common_config.AUTH_TYPE == "oauth2": + logger.info( + "OPAL is running in secure mode - will verify API requests with OAuth2 tokens." + ) + return CachedOAuth2Authenticator(OAuth2ClientCredentialsAuthenticator()) + else: + return JWTAuthenticator(ServerAuthenticatorFactory.__signer()) + + @staticmethod + def __signer() -> JWTSigner: + signer = JWTSigner( + private_key=opal_server_config.AUTH_PRIVATE_KEY, + public_key=opal_common_config.AUTH_PUBLIC_KEY, + algorithm=opal_common_config.AUTH_JWT_ALGORITHM, + audience=opal_common_config.AUTH_JWT_AUDIENCE, + issuer=opal_common_config.AUTH_JWT_ISSUER, + ) + if signer.enabled: + logger.info( + "OPAL is running in secure mode - will verify API requests with JWT tokens." + ) + else: + logger.info( + "OPAL was not provided with JWT encryption keys, cannot verify api requests!" + ) + return signer + + +class WebsocketServerAuthenticatorFactory: + @staticmethod + def create() -> Authenticator: + return WebsocketServerAuthenticator(ServerAuthenticatorFactory.create()) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index da5d043a9..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -6,7 +6,8 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.deps import JWTAuthenticator, get_token_from_header +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -25,7 +26,7 @@ def init_data_updates_router( data_update_publisher: DataUpdatePublisher, data_sources_config: ServerDataSourceConfig, - authenticator: JWTAuthenticator, + authenticator: Authenticator, ): router = APIRouter() diff --git a/packages/opal-server/opal_server/policy/webhook/api.py b/packages/opal-server/opal_server/policy/webhook/api.py index c19595ad2..ef54c81b4 100644 --- a/packages/opal-server/opal_server/policy/webhook/api.py +++ b/packages/opal-server/opal_server/policy/webhook/api.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, Request, status from fastapi_websocket_pubsub.pub_sub_server import PubSubEndpoint -from opal_common.authentication.deps import JWTAuthenticator +from opal_common.authentication.authenticator import Authenticator from opal_common.logger import logger from opal_common.schemas.webhook import GitWebhookRequestParams from opal_server.config import PolicySourceTypes, opal_server_config @@ -15,7 +15,7 @@ def init_git_webhook_router( - pubsub_endpoint: PubSubEndpoint, authenticator: JWTAuthenticator + pubsub_endpoint: PubSubEndpoint, authenticator: Authenticator ): async def dummy_affected_repo_urls(request: Request) -> List[str]: return [] diff --git a/packages/opal-server/opal_server/pubsub.py b/packages/opal-server/opal_server/pubsub.py index c7a3b875e..a5139db92 100644 --- a/packages/opal-server/opal_server/pubsub.py +++ b/packages/opal-server/opal_server/pubsub.py @@ -21,13 +21,12 @@ WebSocketRpcEventNotifier, ) from fastapi_websocket_rpc import RpcChannel -from opal_common.authentication.deps import WebsocketJWTAuthenticator -from opal_common.authentication.signer import JWTSigner from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.confi.confi import load_conf_if_none from opal_common.config import opal_common_config from opal_common.logger import logger +from opal_server.authentication.authenticator import WebsocketServerAuthenticator from opal_server.config import opal_server_config from pydantic import BaseModel from starlette.datastructures import QueryParams @@ -121,7 +120,11 @@ class PubSub: """Wrapper for the Pub/Sub channel used for both policy and data updates.""" - def __init__(self, signer: JWTSigner, broadcaster_uri: str = None): + def __init__( + self, + broadcaster_uri: str = None, + authenticator: Optional[WebsocketServerAuthenticator] = None, + ): """ Args: broadcaster_uri (str, optional): Which server/medium should the PubSub use for broadcasting. Defaults to BROADCAST_URI. @@ -159,7 +162,6 @@ def __init__(self, signer: JWTSigner, broadcaster_uri: str = None): not opal_server_config.BROADCAST_CONN_LOSS_BUGFIX_EXPERIMENT_ENABLED ), ) - authenticator = WebsocketJWTAuthenticator(signer) @self.api_router.get( "/pubsub_client_info", response_model=Dict[str, ClientInfo] diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index d54b1074c..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -16,12 +16,14 @@ from fastapi_websocket_pubsub import PubSubEndpoint from git import InvalidGitRepositoryError from opal_common.async_utils import run_sync +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key -from opal_common.authentication.deps import JWTAuthenticator, get_token_from_header +from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims from opal_common.authentication.verifier import Unauthorized from opal_common.logger import logger @@ -78,7 +80,7 @@ def verify_private_key_or_throw(scope_in: Scope): def init_scope_router( scopes: ScopeRepository, - authenticator: JWTAuthenticator, + authenticator: Authenticator, pubsub_endpoint: PubSubEndpoint, ): router = APIRouter() diff --git a/packages/opal-server/opal_server/security/jwks.py b/packages/opal-server/opal_server/security/jwks.py index 8f19591b9..e128eb903 100644 --- a/packages/opal-server/opal_server/security/jwks.py +++ b/packages/opal-server/opal_server/security/jwks.py @@ -1,5 +1,6 @@ import json from pathlib import Path +from typing import Optional from fastapi import FastAPI from fastapi.staticfiles import StaticFiles @@ -11,7 +12,7 @@ class JwksStaticEndpoint: def __init__( self, - signer: JWTSigner, + signer: Optional[JWTSigner], jwks_url: str, jwks_static_dir: str, ): @@ -25,7 +26,7 @@ def configure_app(self, app: FastAPI): # get the jwks contents from the signer jwks_contents = {} - if self._signer.enabled: + if self._signer is not None and self._signer.enabled: jwk = json.loads(self._signer.get_jwk()) jwks_contents = {"keys": [jwk]} diff --git a/packages/opal-server/opal_server/server.py b/packages/opal-server/opal_server/server.py index f320484ea..397f28446 100644 --- a/packages/opal-server/opal_server/server.py +++ b/packages/opal-server/opal_server/server.py @@ -8,8 +8,8 @@ from fastapi import Depends, FastAPI from fastapi_websocket_pubsub.event_broadcaster import EventBroadcasterContextManager -from opal_common.authentication.deps import JWTAuthenticator, StaticBearerAuthenticator -from opal_common.authentication.signer import JWTSigner +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.deps import StaticBearerAuthenticator from opal_common.confi.confi import load_conf_if_none from opal_common.config import opal_common_config from opal_common.logger import configure_logs, logger @@ -22,6 +22,11 @@ ServerSideTopicPublisher, TopicPublisher, ) +from opal_server.authentication.authenticator import WebsocketServerAuthenticator +from opal_server.authentication.authenticator_factory import ( + ServerAuthenticatorFactory, + WebsocketServerAuthenticatorFactory, +) from opal_server.config import opal_server_config from opal_server.data.api import init_data_updates_router from opal_server.data.data_update_publisher import DataUpdatePublisher @@ -49,7 +54,8 @@ def __init__( init_publisher: bool = None, data_sources_config: Optional[ServerDataSourceConfig] = None, broadcaster_uri: str = None, - signer: Optional[JWTSigner] = None, + authenticator: Optional[Authenticator] = None, + websocketAuthenticator: Optional[WebsocketServerAuthenticator] = None, enable_jwks_endpoint=True, jwks_url: str = None, jwks_static_dir: str = None, @@ -117,33 +123,26 @@ def __init__( self.broadcaster_uri = broadcaster_uri self.master_token = master_token - if signer is not None: - self.signer = signer - else: - self.signer = JWTSigner( - private_key=opal_server_config.AUTH_PRIVATE_KEY, - public_key=opal_common_config.AUTH_PUBLIC_KEY, - algorithm=opal_common_config.AUTH_JWT_ALGORITHM, - audience=opal_common_config.AUTH_JWT_AUDIENCE, - issuer=opal_common_config.AUTH_JWT_ISSUER, - ) - if self.signer.enabled: - logger.info( - "OPAL is running in secure mode - will verify API requests with JWT tokens." - ) + if authenticator is not None: + self.authenticator = authenticator else: - logger.info( - "OPAL was not provided with JWT encryption keys, cannot verify api requests!" - ) + self.authenticator = ServerAuthenticatorFactory.create() if enable_jwks_endpoint: self.jwks_endpoint = JwksStaticEndpoint( - signer=self.signer, jwks_url=jwks_url, jwks_static_dir=jwks_static_dir + signer=self.authenticator.signer(), + jwks_url=jwks_url, + jwks_static_dir=jwks_static_dir, ) else: self.jwks_endpoint = None - self.pubsub = PubSub(signer=self.signer, broadcaster_uri=broadcaster_uri) + _websocketAuthenticator = websocketAuthenticator + if _websocketAuthenticator is None: + _websocketAuthenticator = WebsocketServerAuthenticatorFactory.create() + self.pubsub = PubSub( + broadcaster_uri=broadcaster_uri, authenticator=_websocketAuthenticator + ) self.publisher: Optional[TopicPublisher] = None self.broadcast_keepalive: Optional[PeriodicPublisher] = None @@ -219,19 +218,19 @@ def _configure_monitoring(self): def _configure_api_routes(self, app: FastAPI): """Mounts the api routes on the app object.""" - authenticator = JWTAuthenticator(self.signer) - data_update_publisher: Optional[DataUpdatePublisher] = None if self.publisher is not None: data_update_publisher = DataUpdatePublisher(self.publisher) # Init api routers with required dependencies data_updates_router = init_data_updates_router( - data_update_publisher, self.data_sources_config, authenticator + data_update_publisher, self.data_sources_config, self.authenticator + ) + webhook_router = init_git_webhook_router( + self.pubsub.endpoint, self.authenticator ) - webhook_router = init_git_webhook_router(self.pubsub.endpoint, authenticator) security_router = init_security_router( - self.signer, StaticBearerAuthenticator(self.master_token) + self.authenticator.signer(), StaticBearerAuthenticator(self.master_token) ) statistics_router = init_statistics_router(self.opal_statistics) loadlimit_router = init_loadlimit_router(self.loadlimit_notation) @@ -240,7 +239,7 @@ def _configure_api_routes(self, app: FastAPI): app.include_router( bundles_router, tags=["Bundle Server"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router(data_updates_router, tags=["Data Updates"]) app.include_router(webhook_router, tags=["Github Webhook"]) @@ -249,22 +248,24 @@ def _configure_api_routes(self, app: FastAPI): app.include_router( self.pubsub.api_router, tags=["Pub/Sub"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router( statistics_router, tags=["Server Statistics"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) app.include_router( loadlimit_router, tags=["Client Load Limiting"], - dependencies=[Depends(authenticator)], + dependencies=[Depends(self.authenticator)], ) if opal_server_config.SCOPES: app.include_router( - init_scope_router(self._scopes, authenticator, self.pubsub.endpoint), + init_scope_router( + self._scopes, self.authenticator, self.pubsub.endpoint + ), tags=["Scopes"], prefix="/scopes", ) From ac3ec52aac1a0ed1e8b9c36937dd9a7858611e5b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 101/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 39cca9847f433faec9a39dbe481f69a58ff8c866 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 102/197] Enable OAuth2 authentication. --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 15d23587becf35fe1901f21a4425435ad3c0228e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 103/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3ed9ac586c9688afda6a377f796c8a080083493d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 104/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 07fa5d3fec61d81b20bd9e2eeec73d2db9676a57 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 105/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 988ec664fd01f12d8f718375fbb1cca698aeb729 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 106/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 42028db6b8e881e526539833414aa16276773c79 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 107/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 47917b1a95d1ccc64ab2b49ac68cb8650628aecf Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 108/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 55b295a7df99e4ea981446c9661cc9b3c39a4eff Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 109/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 54a5afc47848445819e0d04a5d85ce1ee46e73e9 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 110/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 35a33343ab17dd8fbdfdd85ef689a2bc195f5975 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 111/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From bb2230477d428ab3a1b23b80b44572473f7e5a49 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 112/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ac12ad652a33a1e21ce2ce9828041e3192609453 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 113/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2f8f710b80da9a3c12bf495b70683973946c9f94 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 114/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 83c718bd42bea25d47931ba0f7e98cf91669a071 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 115/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 11e38c99474abe4eff16254edac5ed49fd28535e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 116/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 4c5c4515c3f9ecfe0d5168fa377ecece55560375 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 117/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ace73c67c9071247a34c2ec12eb5d14161f368bd Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 118/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 61a1a88bc3e035852d559ed3d22231057ff4bd87 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 119/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9c8e035e8cc1b50d3186742e2c85830130ef7cd5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 120/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From c31601051ab59960b69ae6d380d5143b2575f045 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 121/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From dc67665d3191bbd291a67866526d48cda141e756 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 122/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ed8daf8bfe71ca137118d3f3ae0d665fd70127fb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 123/197] Refactor Authenticator to interface and initialize one with provided factory. --- packages/opal-common/opal_common/config.py | 14 +++++++++++--- .../fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 18226c60927d24728de2354dd5dbcd885027b903 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 124/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From f226dfe24663557e8bcce10f9e64524f17836000 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 125/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 4b1610b88767c8e721c6eab8e8dc414c6cf6573d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 126/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5b0f6342631e406c83850d81be57c612ab810fc0 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 127/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6e11cf7b3807ee15294664ac9e416127f31e9ca7 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 128/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8ac9d8560fdf026772500b691c07311c4ce5ed8c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 129/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 7cd52461b90c630a4aff5a867f46637fea233d98 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 130/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From db341a92e0be45905c4ae8350e5c0f685b88419c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 131/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 9bcccc3499f0bfb1d4335bd28d4a4710fd6cabd5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 132/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 7ec2bd724496c023e1375bb95eaaf2df8d6bf5cf Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 133/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3862296f36d929f1150f0337b8ffa9279c1430fd Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 134/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8321a221b566d1668d41ddea11025c5e3b32db4a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 135/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8ef2bed769225c79b76065e0ed4582c841841f6e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 136/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From c8ed2f244fdff727bf0204c60eb617d8e96dae0b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 137/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 76817a5823acf677578aa0dada12050c8593e2bb Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 138/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8f7134ceba23562edd6e699b512a437c08ddee4d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 139/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 1f813b65e346d6ba3f7b854899c0528246746edc Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 140/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From dd4a64526e13efabd2d500f03d1bef5831ebd2e3 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 141/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 325cb947563cd8828c615874d6309afc1dbcb5a1 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 142/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 667d2de078fa2ac6dd06fbc206cea0b0f8401085 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 143/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ec0adef779948700be0ccbc09a460b9d5f70a091 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 144/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b962eb0a7513c8737483dc648a4b96c8c652119d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 145/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2ce69af45b9dbda0bbfd868639a63aa37fdc6881 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 146/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 8750ed56f47499c88d8f3511e74b6616f06ed8e9 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 147/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5f04375a67cabb3d75d6c6b0af2c2e65eccf2a4d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 148/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 044ca592e4d21c3ef84f3d030994aa4a9e0748de Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 149/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e7cccdbe253fa44dcbaa76a1d39cb6939b5ba694 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 150/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From eacab9aca7b77afef5727fbb57fd813c742fa299 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 151/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6753e99c7438854cd371b6f500f07da810c81887 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 152/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d5da478dc7fd5600aa7895e1f1224b3f19690338 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 153/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2802766023b8ae7a78384718ba25869925f79d3d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 154/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From c1090fb919e622fbef4be9e05b2cbf76f02c5488 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 155/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0e9a952a2ee64ea68a8cca62deadbd6816f3097e Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 156/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From aeb3926fcb0d5c0f7abb4c127b1dfd125b3cff72 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 157/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From c6310eac8e78bd410436f6a817e3c56004134f6d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 158/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 31 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 2c3a4a46c8ea0e4d0058c715745cb066a6624cf4 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 159/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 72069f4189cda150c8df67671a665d173b351d04 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 160/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From a22d624723f0ed9b4b310260b0abac9e96e78664 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 161/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0180923cb20ac66c97e5b258f2234d9cd785a34f Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 162/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From eb5d6e8faaa64e563730c65ce281cd0006932bd8 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 163/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From f7ae8ae1e14d7a8fc75bf7d646a1a9c6bf929893 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 164/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ce803a92145ca765e855ba283d5bb2bc46390475 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 165/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 4a1b60b9299737cf477e2b72ba103c5de2c2710c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 166/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5dadf7acc5e8a41295aa828f46492920610a5658 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 167/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b507696d13cba98f5b0302cdf3fba32f9a1da424 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 168/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b590bbfa18eb84e89bd1062b0e1594627a1e398b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 169/197] Reformat code by running pre-commit run --all-files --- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From a5b1a15326771a955d814d362d775f9faee93a19 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 170/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 5 files changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 3a3ebb059fb5c39fa20c827ad8939073655c1f71 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 171/197] Refactor Authenticator to interface and initialize one with provided factory. --- packages/opal-common/opal_common/config.py | 14 +++++++++++--- .../fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ace2c64141d8d8e6899b194e82d80293f89981d8 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 172/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From b376a0e03e1f8fe256285cdc02a0db2e866c2375 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 173/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From af83618a50177860dc3c68e3d15e8f0025d8dd1f Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 174/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6534756674f0b34c32dd1897ad9be2d357658013 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 175/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 77f45c584857e39794b62fe26f991fca389dfa9b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 176/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 92c845a42895088eaa5a00b232fa93860a4cc86b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 177/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d7c4d983f7f73c85bf5846dc48275d1a7d6a66fe Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 178/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 5e6a5ee47d93095361d76e17fe28fd2ee41f3019 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 179/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 487c46ccb9b022fead9475a2eb918136f4c18964 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 180/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 4360dd81a202fe2ca3bd0c7baa0a06fd3e22e1b5 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 181/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-common/opal_common/config.py | 14 ++------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 4ac9b9b39..2b6c7c84a 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,17 +230,9 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int( - "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." - ) - OAUTH2_EXACT_MATCH_CLAIMS = confi.str( - "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." - ) - OAUTH2_REQUIRED_CLAIMS = confi.str( - "OAUTH2_REQUIRED_CLAIMS", - None, - description="Comma separated list of required claims.", - ) + OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") + OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") + OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ab0e6f72e322585afade0c6825f805dd124fd84a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 182/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- packages/opal-common/opal_common/config.py | 14 +++++++-- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 7 files changed, 32 insertions(+), 33 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/config.py b/packages/opal-common/opal_common/config.py index 2b6c7c84a..4ac9b9b39 100644 --- a/packages/opal-common/opal_common/config.py +++ b/packages/opal-common/opal_common/config.py @@ -230,9 +230,17 @@ class OpalCommonConfig(Confi): description="OAuth2 token validation cache TTL.", ) - OAUTH2_EXP_MARGIN = confi.int("OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin.") - OAUTH2_EXACT_MATCH_CLAIMS = confi.str("OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims.") - OAUTH2_REQUIRED_CLAIMS = confi.str("OAUTH2_REQUIRED_CLAIMS", None, description="Comma separated list of required claims.") + OAUTH2_EXP_MARGIN = confi.int( + "OAUTH2_EXP_MARGIN", 5 * 60, description="OAuth2 expiration margin." + ) + OAUTH2_EXACT_MATCH_CLAIMS = confi.str( + "OAUTH2_EXACT_MATCH_CLAIMS", None, description="OAuth2 exact match claims." + ) + OAUTH2_REQUIRED_CLAIMS = confi.str( + "OAUTH2_REQUIRED_CLAIMS", + None, + description="Comma separated list of required claims.", + ) OAUTH2_JWT_ALGORITHM = confi.enum( "OAUTH2_JWT_ALGORITHM", JWTAlgorithm, diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 6662d3c55d796af6bdbf1deab1835858b0bacf78 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 183/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d49dee21a247823f7a22054ed5f9f6a140d17c78 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 184/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From ff287331500dbd77760a9733519f20617d31aa99 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 185/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From eab0551d9d18ccf4e705217275fe6e56bd8c8c03 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 186/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 25afcc91b3741e706eefc0870b67243b0f3a6901 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 187/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From eddccb928c66dc3578a4117c5d70f57835998e3c Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 188/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 547650840ab0fc22812301dc5daa5d1af560e862 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 189/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 0101066540cf808016f6e124177cf5aabbafc8fd Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 190/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From d73ee769877d0e14caa8f74337923f955fe42956 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 191/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 50e3263639fcc6dcc69024063fcaf3f40c935cf9 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 192/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-client/opal_client/data/updater.py # packages/opal-client/opal_client/policy/fetcher.py # packages/opal-client/opal_client/policy/updater.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-server/opal_server/scopes/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From f4f4d62db3c383dd0d5a441c17462c1d0c2d155a Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 2 Sep 2024 13:38:36 +0200 Subject: [PATCH 193/197] Reformat code by running pre-commit run --all-files --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 - 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From e5b25e686a636910d2c5909dfac551b43574f79b Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 194/197] Enable OAuth2 authentication. # Conflicts: # packages/opal-client/opal_client/client.py # packages/opal-common/opal_common/authentication/authenticator.py # packages/opal-common/opal_common/authentication/jwk.py # packages/opal-common/opal_common/authentication/oauth2.py # packages/opal-common/opal_common/config.py # packages/opal-server/opal_server/authentication/authenticator.py # packages/opal-server/opal_server/data/api.py # packages/opal-server/opal_server/server.py # packages/requires.txt --- packages/opal-client/opal_client/client.py | 4 +-- .../opal_common/authentication/jwk.py | 14 ++++----- .../opal_common/authentication/oauth2.py | 29 ++++++++----------- .../fetcher/providers/http_fetch_provider.py | 1 + packages/opal-server/opal_server/data/api.py | 2 +- .../opal-server/opal_server/scopes/api.py | 1 + 6 files changed, 21 insertions(+), 30 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index b6d48ec73..228567216 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,9 +259,7 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api( - self.authenticator, self._callbacks_register - ) + callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index b065235ca..182b5cdb9 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,20 +1,18 @@ -import httpx import jwt +import httpx + from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized - class JWKManager: - def __init__( - self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl - ): + def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header["kid"] + kid = header['kid'] public_key = self._cache.get(kid) if public_key is None: @@ -42,8 +40,6 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..33bc4647a 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,9 +1,8 @@ import asyncio +import httpx import time -from typing import Optional -import httpx -from cachetools import TTLCache, cached +from cachetools import cached, TTLCache from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -12,13 +11,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config - +from typing import Optional class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers["Authorization"] = f"Bearer {token}" + headers['Authorization'] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -62,7 +61,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())["access_token"] + return (response.json())['access_token'] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -80,12 +79,10 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={"token": token}) + response = httpx.post(self._introspect_url, data={'token': token}) if response.status_code != httpx.codes.OK: - raise Unauthorized( - description=f"invalid status code {response.status_code}" - ) + raise Unauthorized(description=f"invalid status code {response.status_code}") claims = response.json() active = claims.get("active", False) @@ -155,15 +152,13 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims['exp'] return self._token - @cached( - cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, - ) - ) + @cached(cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL + )) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index 7fcf05dad..f9a01ed82 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,6 +13,7 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context +from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 6047e9c69..45d953b41 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index f94863deb..6135f5903 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,6 +21,7 @@ require_peer_type, restrict_optional_topics_to_publish, ) +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 548e8a9794fcb8c2889fb4f5d5f6ac009b93ff28 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Wed, 4 Sep 2024 14:23:34 +0200 Subject: [PATCH 195/197] Refactor Authenticator to interface and initialize one with provided factory. --- .../opal_common/fetcher/providers/http_fetch_provider.py | 1 - packages/opal-server/opal_server/scopes/api.py | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py index f9a01ed82..7fcf05dad 100644 --- a/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py +++ b/packages/opal-common/opal_common/fetcher/providers/http_fetch_provider.py @@ -13,7 +13,6 @@ from opal_common.fetcher.logger import get_logger from opal_common.http_utils import is_http_error_response from opal_common.security.sslcontext import get_custom_ssl_context -from opal_common.authentication.authenticator import ClientAuthenticator from pydantic import validator logger = get_logger("http_fetch_provider") diff --git a/packages/opal-server/opal_server/scopes/api.py b/packages/opal-server/opal_server/scopes/api.py index 6135f5903..f94863deb 100644 --- a/packages/opal-server/opal_server/scopes/api.py +++ b/packages/opal-server/opal_server/scopes/api.py @@ -21,7 +21,6 @@ require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.casting import cast_private_key from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import EncryptionKeyFormat, JWTClaims From 570888ae157efd6a24b7071f2ad40efc0687e043 Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Fri, 16 Aug 2024 08:39:10 +0200 Subject: [PATCH 196/197] Enable OAuth2 authentication. --- packages/opal-client/opal_client/client.py | 4 ++- .../opal_client/data/oauth2_updater.py | 27 ++++++++++++----- .../opal_client/tests/data_updater_test.py | 7 ++++- .../opal_common/authentication/jwk.py | 14 +++++---- .../opal_common/authentication/oauth2.py | 29 +++++++++++-------- packages/opal-server/opal_server/data/api.py | 2 +- packages/requires.txt | 1 + 7 files changed, 56 insertions(+), 28 deletions(-) diff --git a/packages/opal-client/opal_client/client.py b/packages/opal-client/opal_client/client.py index 228567216..b6d48ec73 100644 --- a/packages/opal-client/opal_client/client.py +++ b/packages/opal-client/opal_client/client.py @@ -259,7 +259,9 @@ def _configure_api_routes(self, app: FastAPI): policy_router = init_policy_router(policy_updater=self.policy_updater) data_router = init_data_router(data_updater=self.data_updater) policy_store_router = init_policy_store_router(self.authenticator) - callbacks_router = init_callbacks_api(self.authenticator, self._callbacks_register) + callbacks_router = init_callbacks_api( + self.authenticator, self._callbacks_register + ) # mount the api routes on the app object app.include_router(policy_router, tags=["Policy Updater"]) diff --git a/packages/opal-client/opal_client/data/oauth2_updater.py b/packages/opal-client/opal_client/data/oauth2_updater.py index 12adfd47a..55e6aa433 100644 --- a/packages/opal-client/opal_client/data/oauth2_updater.py +++ b/packages/opal-client/opal_client/data/oauth2_updater.py @@ -1,35 +1,46 @@ +from urllib.parse import parse_qs, urlencode, urlparse + import aiohttp from aiohttp.client import ClientSession from opal_client.logger import logger -from urllib.parse import urlencode, urlparse, parse_qs from .updater import DefaultDataUpdater class OAuth2DataUpdater(DefaultDataUpdater): - async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async def _load_policy_data_config( + self, url: str, headers + ) -> aiohttp.ClientResponse: await self._authenticator.authenticate(headers) async with ClientSession(headers=headers) as session: - response = await session.get(url, **self._ssl_context_kwargs, allow_redirects=False) + response = await session.get( + url, **self._ssl_context_kwargs, allow_redirects=False + ) if response.status == 307: - return await self._load_redirected_policy_data_config(response.headers['location'], headers) + return await self._load_redirected_policy_data_config( + response.headers["location"], headers + ) else: return response async def _load_redirected_policy_data_config(self, url: str, headers): redirect_url = self.__redirect_url(url) - logger.info("Redirecting to data-sources configuration '{source}'", source=redirect_url) + logger.info( + "Redirecting to data-sources configuration '{source}'", source=redirect_url + ) async with ClientSession(headers=headers) as session: - return await session.get(redirect_url, **self._ssl_context_kwargs, allow_redirects=False) + return await session.get( + redirect_url, **self._ssl_context_kwargs, allow_redirects=False + ) def __redirect_url(self, url: str) -> str: u = urlparse(url) query = parse_qs(u.query, keep_blank_values=True) - query.pop('token', None) + query.pop("token", None) u = u._replace(query=urlencode(query, True)) - return u.geturl() \ No newline at end of file + return u.geturl() diff --git a/packages/opal-client/opal_client/tests/data_updater_test.py b/packages/opal-client/opal_client/tests/data_updater_test.py index eccd82a97..df342a308 100644 --- a/packages/opal-client/opal_client/tests/data_updater_test.py +++ b/packages/opal-client/opal_client/tests/data_updater_test.py @@ -21,7 +21,12 @@ from opal_client.config import opal_client_config from opal_client.data.rpc import TenantAwareRpcEventClientMethods -from opal_client.data.updater import DataSourceEntry, DataUpdate, DataUpdater, DefaultDataUpdater +from opal_client.data.updater import ( + DataSourceEntry, + DataUpdate, + DataUpdater, + DefaultDataUpdater, +) from opal_client.policy_store.policy_store_client_factory import ( PolicyStoreClientFactory, ) diff --git a/packages/opal-common/opal_common/authentication/jwk.py b/packages/opal-common/opal_common/authentication/jwk.py index 182b5cdb9..b065235ca 100644 --- a/packages/opal-common/opal_common/authentication/jwk.py +++ b/packages/opal-common/opal_common/authentication/jwk.py @@ -1,18 +1,20 @@ -import jwt import httpx - +import jwt from cachetools import TTLCache from opal_common.authentication.verifier import Unauthorized + class JWKManager: - def __init__(self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl): + def __init__( + self, openid_configuration_url, jwt_algorithm, cache_maxsize, cache_ttl + ): self._openid_configuration_url = openid_configuration_url self._jwt_algorithm = jwt_algorithm self._cache = TTLCache(maxsize=cache_maxsize, ttl=cache_ttl) def public_key(self, token): header = jwt.get_unverified_header(token) - kid = header['kid'] + kid = header["kid"] public_key = self._cache.get(kid) if public_key is None: @@ -40,6 +42,8 @@ def _openid_configuration(self): response = httpx.get(self._openid_configuration_url) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) return response.json() diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 33bc4647a..237d01293 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -1,8 +1,9 @@ import asyncio -import httpx import time +from typing import Optional -from cachetools import cached, TTLCache +import httpx +from cachetools import TTLCache, cached from fastapi import Header from httpx import AsyncClient, BasicAuth from opal_common.authentication.authenticator import Authenticator @@ -11,13 +12,13 @@ from opal_common.authentication.signer import JWTSigner from opal_common.authentication.verifier import JWTVerifier, Unauthorized from opal_common.config import opal_common_config -from typing import Optional + class _OAuth2Authenticator(Authenticator): async def authenticate(self, headers): if "Authorization" not in headers: token = await self.token() - headers['Authorization'] = f"Bearer {token}" + headers["Authorization"] = f"Bearer {token}" class OAuth2ClientCredentialsAuthenticator(_OAuth2Authenticator): @@ -61,7 +62,7 @@ async def token(self): async with AsyncClient() as client: response = await client.post(self._token_url, auth=auth, data=data) - return (response.json())['access_token'] + return (response.json())["access_token"] def __call__(self, authorization: Optional[str] = Header(None)) -> {}: token = get_token_from_header(authorization) @@ -79,10 +80,12 @@ def verify(self, token: str) -> {}: return claims def _verify_opaque(self, token: str) -> {}: - response = httpx.post(self._introspect_url, data={'token': token}) + response = httpx.post(self._introspect_url, data={"token": token}) if response.status_code != httpx.codes.OK: - raise Unauthorized(description=f"invalid status code {response.status_code}") + raise Unauthorized( + description=f"invalid status code {response.status_code}" + ) claims = response.json() active = claims.get("active", False) @@ -152,13 +155,15 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims['exp'] + self._exp = claims["exp"] return self._token - @cached(cache=TTLCache( - maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, - ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL - )) + @cached( + cache=TTLCache( + maxsize=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_MAXSIZE, + ttl=opal_common_config.OAUTH2_TOKEN_VERIFY_CACHE_TTL, + ) + ) def __call__(self, authorization: Optional[str] = Header(None)) -> {}: return self._delegate(authorization) diff --git a/packages/opal-server/opal_server/data/api.py b/packages/opal-server/opal_server/data/api.py index 45d953b41..6047e9c69 100644 --- a/packages/opal-server/opal_server/data/api.py +++ b/packages/opal-server/opal_server/data/api.py @@ -2,11 +2,11 @@ from fastapi import APIRouter, Depends, Header, HTTPException, status from fastapi.responses import RedirectResponse +from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authz import ( require_peer_type, restrict_optional_topics_to_publish, ) -from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.deps import get_token_from_header from opal_common.authentication.types import JWTClaims from opal_common.authentication.verifier import Unauthorized diff --git a/packages/requires.txt b/packages/requires.txt index 84d128f45..6e99bc663 100644 --- a/packages/requires.txt +++ b/packages/requires.txt @@ -13,3 +13,4 @@ fastapi-utils>=0.2.1,<1 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability starlette>=0.40.0 # not directly required, pinned by Snyk to avoid a vulnerability +tls-cert-refresh-period From 1fc33ae5c9b1dc8f792ae4dbf894de57b273779d Mon Sep 17 00:00:00 2001 From: Ondrej Scecina Date: Mon, 25 Aug 2025 10:35:08 +0200 Subject: [PATCH 197/197] Enable OAuth2 authentication. --- .../opal-client/opal_client/data/updater.py | 76 +++++++++++-------- .../opal-client/opal_client/policy/fetcher.py | 3 +- .../opal-client/opal_client/policy/updater.py | 13 ++-- .../opal_client/policy_store/opa_client.py | 2 +- .../opal_common/authentication/deps.py | 3 +- .../opal_common/authentication/oauth2.py | 2 +- packages/requires.txt | 1 + 7 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/opal-client/opal_client/data/updater.py b/packages/opal-client/opal_client/data/updater.py index 9cd6503ce..93d39ad34 100644 --- a/packages/opal-client/opal_client/data/updater.py +++ b/packages/opal-client/opal_client/data/updater.py @@ -39,11 +39,45 @@ from opal_common.schemas.store import TransactionType from opal_common.security.sslcontext import get_custom_ssl_context from opal_common.synchronization.hierarchical_lock import HierarchicalLock -from opal_common.utils import get_authorization_header +from opal_common.utils import get_authorization_header, tuple_to_dict from pydantic.json import pydantic_encoder class DataUpdater: + async def get_base_policy_data( + self, config_url: str = None, data_fetch_reason="Initial load" + ): + pass + + async def stop(self): + pass + + async def wait_until_done(self): + pass + + @staticmethod + def calc_hash(data: JsonableValue) -> str: + """Calculates a SHA-256 hash of the given data to be used to identify + the updates (e.g. in logging reports on the transactions) . If 'data' + is not a string, it is first serialized to JSON. Returns an empty + string on failure. + + Args: + data (JsonableValue): The data to be hashed. + + Returns: + str: The hexadecimal representation of the SHA-256 hash. + """ + try: + if not isinstance(data, str): + data = json.dumps(data, default=pydantic_encoder) + return hashlib.sha256(data.encode("utf-8")).hexdigest() + except Exception as e: + logger.exception(f"Failed to calculate hash for data {data}: {e}") + return "" + + +class DefaultDataUpdater(DataUpdater): """The DataUpdater is responsible for synchronizing data sources with the policy store (e.g. OPA). It listens to Pub/Sub topics for data updates, fetches the updated data, and writes it into the policy store. The updater @@ -142,11 +176,11 @@ def __init__( self._opal_client_id = opal_client_id # Prepare any extra headers (token, shard id, etc.) - self._extra_headers = [] - if self._token is not None: - self._extra_headers.append(get_authorization_header(self._token)) + self._extra_headers = {} + if self._token is not None and opal_common_config.AUTH_TYPE != "oauth2": + self._extra_headers = tuple_to_dict(get_authorization_header(self._token)) if self._shard_id is not None: - self._extra_headers.append(("X-Shard-ID", self._shard_id)) + self._extra_headers["X-Shard-ID"] = self._shard_id if len(self._extra_headers) == 0: self._extra_headers = None @@ -236,11 +270,11 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: url = self._data_sources_config_url logger.info("Getting data-sources configuration from '{source}'", source=url) - headers = {} if self._extra_headers is not None: headers = self._extra_headers.copy() - headers['Accept'] = "application/json" + headers["Accept"] = "application/json" + await self._authenticator.authenticate(headers) try: response = await self._load_policy_data_config(url, headers) @@ -256,7 +290,9 @@ async def get_policy_data_config(self, url: str = None) -> DataSourceConfig: logger.exception("Failed to load data sources config") raise - async def _load_policy_data_config(self, url: str, headers) -> aiohttp.ClientResponse: + async def _load_policy_data_config( + self, url: str, headers + ) -> aiohttp.ClientResponse: async with ClientSession(headers=headers, trust_env=True) as session: return await session.get(url, **self._ssl_context_kwargs) @@ -370,8 +406,7 @@ async def _subscriber(self): methods_class=TenantAwareRpcEventClientMethods, on_connect=[self.on_connect, *self._on_connect_callbacks], on_disconnect=[self.on_disconnect, *self._on_disconnect_callbacks], - additional_headers=self._extra_headers, - extra_headers=headers, + additional_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, @@ -445,27 +480,6 @@ async def wait_until_done(self): if self._subscriber_task is not None: await self._subscriber_task - @staticmethod - def calc_hash(data: JsonableValue) -> str: - """Calculates a SHA-256 hash of the given data to be used to identify - the updates (e.g. in logging reports on the transactions) . If 'data' - is not a string, it is first serialized to JSON. Returns an empty - string on failure. - - Args: - data (JsonableValue): The data to be hashed. - - Returns: - str: The hexadecimal representation of the SHA-256 hash. - """ - try: - if not isinstance(data, str): - data = json.dumps(data, default=pydantic_encoder) - return hashlib.sha256(data.encode("utf-8")).hexdigest() - except Exception as e: - logger.exception(f"Failed to calculate hash for data {data}: {e}") - return "" - async def _update_policy_data(self, update: DataUpdate) -> None: """Performs the core data update process for the given DataUpdate object. diff --git a/packages/opal-client/opal_client/policy/fetcher.py b/packages/opal-client/opal_client/policy/fetcher.py index c309ed6ba..85856f4bd 100644 --- a/packages/opal-client/opal_client/policy/fetcher.py +++ b/packages/opal-client/opal_client/policy/fetcher.py @@ -6,6 +6,7 @@ from opal_client.logger import logger from opal_common.authentication.authenticator import Authenticator from opal_common.authentication.authenticator_factory import AuthenticatorFactory +from opal_common.config import opal_common_config from opal_common.schemas.policy import PolicyBundle from opal_common.security.sslcontext import get_custom_ssl_context from opal_common.utils import ( @@ -47,7 +48,7 @@ def __init__( self._authenticator = AuthenticatorFactory.create() self._token = token or opal_client_config.CLIENT_TOKEN self._backend_url = backend_url or opal_client_config.SERVER_URL - if self._token is not None: + if self._token is not None and opal_common_config.AUTH_TYPE != "oauth2": self._auth_headers = tuple_to_dict(get_authorization_header(self._token)) else: self._auth_headers = dict() diff --git a/packages/opal-client/opal_client/policy/updater.py b/packages/opal-client/opal_client/policy/updater.py index 07c57c6a4..da4031fbd 100644 --- a/packages/opal-client/opal_client/policy/updater.py +++ b/packages/opal-client/opal_client/policy/updater.py @@ -17,13 +17,15 @@ DEFAULT_POLICY_STORE_GETTER, ) from opal_common.async_utils import TakeANumberQueue, TasksPool +from opal_common.authentication.authenticator import Authenticator +from opal_common.authentication.authenticator_factory import AuthenticatorFactory from opal_common.config import opal_common_config from opal_common.schemas.data import DataUpdateReport from opal_common.schemas.policy import PolicyBundle, PolicyUpdateMessage from opal_common.schemas.store import TransactionType from opal_common.security.sslcontext import get_custom_ssl_context from opal_common.topics.utils import pubsub_topics_from_directories -from opal_common.utils import get_authorization_header +from opal_common.utils import get_authorization_header, tuple_to_dict class PolicyUpdater: @@ -77,10 +79,11 @@ def __init__( # pub/sub server url and authentication data self._server_url = pubsub_url self._token = token - if self._token is None: + self._extra_headers = {} + if self._token is not None and opal_common_config.AUTH_TYPE != "oauth2": + self._extra_headers = tuple_to_dict(get_authorization_header(self._token)) + if len(self._extra_headers) == 0: self._extra_headers = None - else: - self._extra_headers = [get_authorization_header(self._token)] # Pub/Sub topics we subscribe to for policy updates if self._scope_id == "default": self._topics = pubsub_topics_from_directories( @@ -261,7 +264,7 @@ async def _subscriber(self): callback=self._update_policy_callback, on_connect=[self._on_connect, *self._on_connect_callbacks], on_disconnect=[self._on_disconnect, *self._on_disconnect_callbacks], - extra_headers=headers, + additional_headers=headers, keep_alive=opal_client_config.KEEP_ALIVE_INTERVAL, server_uri=self._server_url, **self._ssl_context_kwargs, diff --git a/packages/opal-client/opal_client/policy_store/opa_client.py b/packages/opal-client/opal_client/policy_store/opa_client.py index bf9929f8a..9cfc0d611 100644 --- a/packages/opal-client/opal_client/policy_store/opa_client.py +++ b/packages/opal-client/opal_client/policy_store/opa_client.py @@ -430,7 +430,7 @@ async def _get_oauth_token(self): ) as oauth_response: response = await oauth_response.json() logger.info( - f"got access_token, expires in {response['expires_in']} seconds" + f"Got access_token, expires in {response['expires_in']} seconds" ) return { diff --git a/packages/opal-common/opal_common/authentication/deps.py b/packages/opal-common/opal_common/authentication/deps.py index 332a37cb7..73ec14a3a 100644 --- a/packages/opal-common/opal_common/authentication/deps.py +++ b/packages/opal-common/opal_common/authentication/deps.py @@ -78,10 +78,11 @@ def verifier(self) -> JWTVerifier: return self._verifier def signer(self) -> Optional[JWTSigner]: + logger.info("!!!!! signer == _verifier") return self._verifier @property - def enabled(self) -> JWTVerifier: + def enabled(self) -> bool: return self._verifier.enabled async def authenticate(self, headers): diff --git a/packages/opal-common/opal_common/authentication/oauth2.py b/packages/opal-common/opal_common/authentication/oauth2.py index 237d01293..16c068409 100644 --- a/packages/opal-common/opal_common/authentication/oauth2.py +++ b/packages/opal-common/opal_common/authentication/oauth2.py @@ -155,7 +155,7 @@ async def token(self): claims = self._delegate.verify(token) self._token = token - self._exp = claims["exp"] + self._exp = claims.get("exp") return self._token diff --git a/packages/requires.txt b/packages/requires.txt index 84d128f45..34b83ee39 100644 --- a/packages/requires.txt +++ b/packages/requires.txt @@ -13,3 +13,4 @@ fastapi-utils>=0.2.1,<1 setuptools>=70.0.0 # not directly required, pinned by Snyk to avoid a vulnerability anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability starlette>=0.40.0 # not directly required, pinned by Snyk to avoid a vulnerability +cachetools>=5.3.3