ARTIFACT-STORE-WP-0007 D7.4: STS temporary credential support (session token + refreshable file refs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 11:23:44 +02:00
parent 68a5ff0ba2
commit 8fbce69475
8 changed files with 170 additions and 14 deletions

View File

@@ -46,16 +46,24 @@ def _build_backends(settings: Settings) -> dict[str, StorageBackend]:
if "local" in configured:
backends["local"] = LocalBackend(settings.storage_local_root, backend_id="local")
if "s3" in configured:
access_key = (
resolve_secret_ref(settings.s3_access_key_ref)
if settings.s3_access_key_ref
else None
)
secret_key = (
resolve_secret_ref(settings.s3_secret_key_ref)
if settings.s3_secret_key_ref
else None
refs = (
settings.s3_access_key_ref,
settings.s3_secret_key_ref,
settings.s3_session_token_ref,
)
def _resolve_credentials() -> tuple[str | None, str | None, str | None]:
access_ref, secret_ref, token_ref = refs
return (
resolve_secret_ref(access_ref) if access_ref else None,
resolve_secret_ref(secret_ref) if secret_ref else None,
resolve_secret_ref(token_ref) if token_ref else None,
)
access_key, secret_key, session_token = _resolve_credentials()
# file: refs are re-read per client so an STS sidecar/controller can
# rotate all three values atomically without a process restart.
refreshable = any(ref.startswith("file:") for ref in refs if ref)
backends["s3"] = S3Backend(
S3BackendConfig(
endpoint_url=settings.s3_endpoint_url,
@@ -64,11 +72,13 @@ def _build_backends(settings: Settings) -> dict[str, StorageBackend]:
key_prefix=settings.s3_key_prefix,
access_key_id=access_key,
secret_access_key=secret_key,
session_token=session_token,
storage_class=settings.s3_storage_class or None,
sse=settings.s3_sse or None,
multipart_threshold_bytes=settings.s3_multipart_threshold_bytes,
multipart_chunk_bytes=settings.s3_multipart_chunk_bytes,
)
),
credentials_provider=_resolve_credentials if refreshable else None,
)
unknown = set(configured) - set(backends)
if unknown:

View File

@@ -62,6 +62,7 @@ class Settings(BaseSettings):
s3_key_prefix: str = ""
s3_access_key_ref: str = ""
s3_secret_key_ref: str = ""
s3_session_token_ref: str = ""
s3_storage_class: str = ""
s3_sse: str = ""
s3_multipart_threshold_bytes: int = 64 * 1024 * 1024

View File

@@ -29,6 +29,7 @@ class S3BackendConfig:
key_prefix: str = ""
access_key_id: str | None = None
secret_access_key: str | None = None
session_token: str | None = None
storage_class: str | None = None
sse: str | None = None
multipart_threshold_bytes: int = 64 * 1024 * 1024
@@ -37,6 +38,11 @@ class S3BackendConfig:
ClientFactory = Callable[[], AbstractAsyncContextManager[Any]]
# Returns (access_key_id, secret_access_key, session_token) for each new
# client, so STS-vended credentials rotated by a sidecar/controller (e.g.
# re-written mounted files) are picked up without a process restart.
CredentialsProvider = Callable[[], tuple[str | None, str | None, str | None]]
class S3Backend:
"""Storage SPI implementation over an S3-compatible object store."""
@@ -47,6 +53,7 @@ class S3Backend:
*,
backend_id: str = "s3",
client_factory: ClientFactory | None = None,
credentials_provider: CredentialsProvider | None = None,
chunk_size: int = _DEFAULT_CHUNK_SIZE,
) -> None:
if not config.bucket:
@@ -54,6 +61,7 @@ class S3Backend:
self._config = config
self._backend_id = backend_id
self._client_factory = client_factory
self._credentials_provider = credentials_provider
self._chunk_size = chunk_size
@property
@@ -69,9 +77,16 @@ class S3Backend:
raise RuntimeError(
"S3Backend requires the 'aioboto3' package; install artifactstore[s3]"
) from exc
if self._credentials_provider is not None:
access_key_id, secret_access_key, session_token = self._credentials_provider()
else:
access_key_id = self._config.access_key_id
secret_access_key = self._config.secret_access_key
session_token = self._config.session_token
session = aioboto3.Session(
aws_access_key_id=self._config.access_key_id,
aws_secret_access_key=self._config.secret_access_key,
aws_access_key_id=access_key_id,
aws_secret_access_key=secret_access_key,
aws_session_token=session_token,
region_name=self._config.region,
)
return cast(