From 20fea2560ff467b07c47d558f55b70194760c776 Mon Sep 17 00:00:00 2001 From: Matt Hammerly Date: Mon, 2 Mar 2026 17:54:22 -0800 Subject: [PATCH] feat(objectstore): enable objectstore auth if key is configured --- src/launchpad/artifact_processor.py | 5 ++- src/launchpad/service.py | 25 +++++++++++++-- src/launchpad/utils/objectstore.py | 50 +++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 src/launchpad/utils/objectstore.py diff --git a/src/launchpad/artifact_processor.py b/src/launchpad/artifact_processor.py index 3a1e2384..7202c7b6 100644 --- a/src/launchpad/artifact_processor.py +++ b/src/launchpad/artifact_processor.py @@ -48,6 +48,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__) @@ -93,9 +94,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) if service_config and project_id in service_config.projects_to_skip: diff --git a/src/launchpad/service.py b/src/launchpad/service.py index 84a2722b..8c22aaa1 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,23 @@ 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 + + @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 +147,14 @@ 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 sentry_base_url is None: sentry_base_url = "http://getsentry.default" @@ -143,7 +162,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..7a410720 --- /dev/null +++ b/src/launchpad/utils/objectstore.py @@ -0,0 +1,50 @@ +from objectstore_client import ( + Client as ObjectstoreClient, +) +from objectstore_client import ( + Permission, + TokenGenerator, +) + +import launchpad + +from launchpad.utils.logging import get_logger + +logger = get_logger(__name__) + +_cached_keyfile: str | None = None + +TOKEN_PERMISSIONS: list[Permission] = [ + Permission.OBJECT_READ, + Permission.OBJECT_WRITE, + Permission.OBJECT_DELETE, +] + + +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: "launchpad.service.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, + TOKEN_PERMISSIONS, + ) + + return ObjectstoreClient(config.objectstore_url, token_generator=token_generator)