From 422a71f36009935877373a4972df17d5faffc07d Mon Sep 17 00:00:00 2001 From: Matt Hammerly Date: Thu, 8 Jan 2026 16:48:43 -0800 Subject: [PATCH] feat(objectstore): enable objectstore auth if key is configured --- requirements.txt | 2 +- src/launchpad/artifact_processor.py | 5 ++-- src/launchpad/service.py | 30 ++++++++++++++++++--- src/launchpad/utils/objectstore.py | 42 +++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 src/launchpad/utils/objectstore.py diff --git a/requirements.txt b/requirements.txt index e398dd58..eb9a69d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ google-api-core==2.25.1 google-api-python-client==2.181.0 lief==0.17.2 lzfse>=0.4.2 -objectstore-client>=0.0.14 +objectstore-client>=0.0.15 pillow>=11.3.0 pillow_heif>=1.1.0 protobuf>=5.29.5,<6 diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index c45beb65..4f457418 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -47,6 +47,7 @@ from launchpad.tracing import request_context from launchpad.utils.file_utils import IdPrefix, id_from_bytes from launchpad.utils.logging import get_logger +from launchpad.utils.objectstore import create_objectstore_client from launchpad.utils.statsd import StatsdInterface, get_statsd logger = get_logger(__name__) @@ -92,9 +93,7 @@ def process_message( statsd = get_statsd() if artifact_processor is None: sentry_client = SentryClient(base_url=service_config.sentry_base_url) - objectstore_client = None - if service_config.objectstore_url is not None: - objectstore_client = ObjectstoreClient(service_config.objectstore_url) + objectstore_client = create_objectstore_client(service_config.objectstore_config) artifact_processor = ArtifactProcessor(sentry_client, statsd, objectstore_client) requested_features = [] diff --git a/src/launchpad/service.py b/src/launchpad/service.py index 84a2722b..43c019d8 100644 --- a/src/launchpad/service.py +++ b/src/launchpad/service.py @@ -9,6 +9,8 @@ from dataclasses import dataclass +from objectstore_client import Permission as ObjectstorePermission + from launchpad.sentry_client import SentryClient from launchpad.utils.logging import get_logger from launchpad.utils.statsd import NullStatsd, StatsdInterface, get_statsd @@ -121,13 +123,24 @@ def _shutdown_server(self) -> None: self._server_thread.join(timeout=10) +@dataclass +class ObjectstoreConfig: + """Objectstore client configuration data.""" + + objectstore_url: str | None + key_id: str | None = None + key_file: str | None = None + token_expiry_seconds: int = 60 + token_permissions: list[ObjectstorePermission] = ObjectstorePermission.max() + + @dataclass class ServiceConfig: """Service configuration data.""" sentry_base_url: str projects_to_skip: list[str] - objectstore_url: str | None + objectstore_config: ObjectstoreConfig def get_service_config() -> ServiceConfig: @@ -135,7 +148,18 @@ def get_service_config() -> ServiceConfig: sentry_base_url = os.getenv("SENTRY_BASE_URL") projects_to_skip_str = os.getenv("PROJECT_IDS_TO_SKIP") projects_to_skip = projects_to_skip_str.split(",") if projects_to_skip_str else [] - objectstore_url = os.getenv("OBJECTSTORE_URL") + + objectstore_config = ObjectstoreConfig( + objectstore_url=os.getenv("OBJECTSTORE_URL"), + key_id=os.getenv("OBJECTSTORE_SIGNING_KEY_ID"), + key_file=os.getenv("OBJECTSTORE_SIGNING_KEY_FILE"), + ) + if expiry_seconds := os.getenv("OBJECTSTORE_TOKEN_EXPIRY_SECONDS"): + objectstore_config.token_expiry_seconds = int(expiry_seconds) + if permissions := os.getenv("OBJECTSTORE_TOKEN_PERMISSIONS"): + objectstore_config.token_permissions = [ + ObjectstorePermission(permission) for permission in permissions.split(",") + ] if sentry_base_url is None: sentry_base_url = "http://getsentry.default" @@ -143,7 +167,7 @@ def get_service_config() -> ServiceConfig: return ServiceConfig( sentry_base_url=sentry_base_url, projects_to_skip=projects_to_skip, - objectstore_url=objectstore_url, + objectstore_config=objectstore_config, ) diff --git a/src/launchpad/utils/objectstore.py b/src/launchpad/utils/objectstore.py new file mode 100644 index 00000000..660405d5 --- /dev/null +++ b/src/launchpad/utils/objectstore.py @@ -0,0 +1,42 @@ +from objectstore_client import ( + Client as ObjectstoreClient, +) +from objectstore_client import ( + TokenGenerator, +) + +from launchpad.service import ObjectstoreConfig +from launchpad.utils.logging import get_logger + +logger = get_logger(__name__) + +_cached_keyfile: str | None = None + + +def _read_keyfile(path: str) -> str | None: + global _cached_keyfile + if not _cached_keyfile: + try: + with open(path) as f: + _cached_keyfile = f.read() + except Exception: + logger.exception(f"Failed to load objectstore keyfile at {path}") + + return _cached_keyfile + + +def create_objectstore_client(config: ObjectstoreConfig) -> ObjectstoreClient | None: + if not config.objectstore_url: + return None + + token_generator = None + if config.key_id and config.key_file: + if secret_key := _read_keyfile(config.key_file): + token_generator = TokenGenerator( + config.key_id, + secret_key, + config.token_expiry_seconds, + config.token_permissions, + ) + + return ObjectstoreClient(config.objectstore_url, token_generator=token_generator)