mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(bedrock): sign batch S3 requests with s3_access_key_id and s3_secret_access_key (#42342)
* fix(bedrock): sign batch S3 requests with s3_access_key_id and s3_secret_access_key Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(bedrock): keep S3 signer test additions scoped to new cases Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(bedrock): drop e2e suite changes from the S3 signing fix Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(bedrock): build S3 credentials directly from the s3_* pair so ambient AWS_* env never mixes in Restores the split-identity e2e coverage Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yucheng <yucheng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
b720909dac
commit
5dc6261ebb
13 changed files with 257 additions and 7 deletions
|
|
@ -46,6 +46,8 @@ OPTIONAL_KWARGS_KEYS: Final = (
|
|||
"bucket_name",
|
||||
"s3_endpoint_url",
|
||||
"s3_region_name",
|
||||
"s3_access_key_id",
|
||||
"s3_secret_access_key",
|
||||
"vertex_credentials",
|
||||
"vertex_project",
|
||||
"vertex_location",
|
||||
|
|
|
|||
|
|
@ -524,6 +524,17 @@ class BaseAWSLLM(SignsRequestsWithAWS):
|
|||
aws_session_tags=_canonical_aws_session_tags(auth_params.aws_session_tags),
|
||||
)
|
||||
|
||||
def resolve_s3_credentials(self, params: Mapping[str, object], aws_region_name: str | None) -> Credentials:
|
||||
"""S3 signing identity: the s3_* static pair as-is when both are set, otherwise the resolved aws_* params."""
|
||||
from botocore.credentials import Credentials
|
||||
|
||||
from litellm.llms.bedrock.common_utils import s3_static_key_pair
|
||||
|
||||
s3_pair: Final = s3_static_key_pair(params)
|
||||
if s3_pair is None:
|
||||
return self.resolve_credentials(AwsAuthParams.model_validate(params), aws_region_name)
|
||||
return Credentials(access_key=s3_pair[0], secret_key=s3_pair[1])
|
||||
|
||||
def _get_aws_region_from_model_arn(self, model: str | None) -> str | None:
|
||||
try:
|
||||
# First check if the string contains the expected prefix
|
||||
|
|
|
|||
|
|
@ -112,6 +112,17 @@ def merge_bedrock_aws_request_params(
|
|||
return request_params
|
||||
|
||||
|
||||
def s3_static_key_pair(params: Mapping[str, object]) -> tuple[str, str] | None:
|
||||
"""The s3_access_key_id / s3_secret_access_key pair when both are set, otherwise None."""
|
||||
s3_access_key_id: Final = params.get("s3_access_key_id")
|
||||
s3_secret_access_key: Final = params.get("s3_secret_access_key")
|
||||
if not isinstance(s3_access_key_id, str) or not s3_access_key_id:
|
||||
return None
|
||||
if not isinstance(s3_secret_access_key, str) or not s3_secret_access_key:
|
||||
return None
|
||||
return s3_access_key_id, s3_secret_access_key
|
||||
|
||||
|
||||
# Lazy import cache to avoid circular imports and performance impact
|
||||
_get_model_info = None
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from litellm.litellm_core_utils.cloud_storage_security import (
|
|||
validate_managed_cloud_file_id,
|
||||
)
|
||||
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||
from litellm.types.llms.bedrock import AwsAuthParams
|
||||
from litellm.types.llms.openai import (
|
||||
FileContentRequest,
|
||||
HttpxBinaryResponseContent,
|
||||
|
|
@ -103,9 +102,7 @@ class BedrockFilesHandler(BaseAWSLLM):
|
|||
)
|
||||
|
||||
aws_region_name: Final = self._get_aws_region_name(optional_params=optional_params, model="")
|
||||
credentials: Final[Credentials] = self.resolve_credentials(
|
||||
AwsAuthParams.model_validate(optional_params), aws_region_name
|
||||
)
|
||||
credentials: Final[Credentials] = self.resolve_s3_credentials(optional_params, aws_region_name)
|
||||
|
||||
# Create S3 client
|
||||
s3_client: Final = boto3.client(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,11 @@ from litellm.types.utils import ExtractedFileData, LlmProviders, SpecialEnums
|
|||
from litellm.utils import get_llm_provider
|
||||
|
||||
from ..base_aws_llm import BaseAWSLLM
|
||||
from ..common_utils import BedrockError, merge_bedrock_aws_request_params, resolve_s3_encryption_key_id
|
||||
from ..common_utils import (
|
||||
BedrockError,
|
||||
merge_bedrock_aws_request_params,
|
||||
resolve_s3_encryption_key_id,
|
||||
)
|
||||
|
||||
S3_SIGNED_REQUEST_HEADERS_PARAM: Final = "_s3_signed_request_headers"
|
||||
|
||||
|
|
@ -148,6 +152,8 @@ class _BedrockS3RequestParams(AwsAuthParams):
|
|||
aws_region_name: str | None = None
|
||||
s3_region_name: str | None = None
|
||||
s3_endpoint_url: str | None = None
|
||||
s3_access_key_id: str | None = None
|
||||
s3_secret_access_key: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -1147,7 +1153,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
raise ImportError("Missing boto3 to call bedrock. Run 'pip install boto3'.")
|
||||
|
||||
aws_region_name: Final = self._get_aws_region_name(optional_params=optional_params, model="")
|
||||
credentials: Final = self.resolve_credentials(AwsAuthParams.model_validate(optional_params), aws_region_name)
|
||||
credentials: Final = self.resolve_s3_credentials(optional_params, aws_region_name)
|
||||
|
||||
# Calculate SHA256 hash of the content (REQUIRED for S3)
|
||||
content_hash: Final = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||
|
|
@ -1494,7 +1500,7 @@ class BedrockFilesConfig(BaseAWSLLM, BaseFilesConfig):
|
|||
except ImportError:
|
||||
raise ImportError("Missing boto3 to call bedrock. Run 'pip install boto3'.")
|
||||
|
||||
credentials: Final = self.resolve_credentials(request_params, aws_region_name)
|
||||
credentials: Final = self.resolve_s3_credentials(request_params.model_dump(exclude_none=True), aws_region_name)
|
||||
|
||||
empty_body_hash: Final = hashlib.sha256(b"").hexdigest()
|
||||
aws_request: Final = AWSRequest( # any-ok: botocore AWSRequest is untyped
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ failures are hard test failures (see `tests/e2e/AGENTS.md`).
|
|||
| Vertex AI | yes | yes | yes | yes | yes (provider-transformed) | GCS (`gcs_bucket_name` / `GCS_BUCKET_NAME` on model) |
|
||||
| Bedrock | yes (unified only) | yes | yes | yes (unfiltered managed list) | yes (provider-transformed) | S3 (`s3_bucket_name` + `aws_*` + `AWS_BATCH_ROLE_ARN` on model) |
|
||||
| Bedrock GovCloud (`us-gov-west-1`) | yes (unified only) | yes | no | no | yes (provider-transformed) | S3 (`s3_bucket_name` + `aws_*` on model, resolved from `AWS_GOVCLOUD_ACCESS_KEY_ID` / `AWS_GOVCLOUD_SECRET_ACCESS_KEY` / `AWS_GOVCLOUD_BATCH_S3_BUCKET` / `AWS_GOVCLOUD_BATCH_ROLE_ARN`) |
|
||||
| Bedrock split S3 identity | no | no | no | no | yes (file upload, content, delete) | S3 signed with `s3_access_key_id` / `s3_secret_access_key` (`AWS_S3_ONLY_ACCESS_KEY_ID` / `AWS_S3_ONLY_SECRET_ACCESS_KEY`, object rights on `AWS_BATCH_S3_BUCKET` only) while `aws_*` is `AWS_BEDROCK_ONLY_ACCESS_KEY_ID` / `AWS_BEDROCK_ONLY_SECRET_ACCESS_KEY`, an identity with no S3 rights on that bucket |
|
||||
|
||||
Bedrock cancel maps to `StopModelInvocationJob` and comes back `cancelling`; the
|
||||
lifecycle asserts it the same way it does for OpenAI (`_CANCEL_ASSERTED_PROVIDERS`).
|
||||
|
|
|
|||
|
|
@ -1024,6 +1024,72 @@ class TestBedrockBatchAssumeRole:
|
|||
assert fetched.id == batch.id
|
||||
|
||||
|
||||
def _split_s3_identity_params() -> LiteLLMParamsBody:
|
||||
return LiteLLMParamsBody(
|
||||
model=ASSUME_ROLE_RAW_MODEL,
|
||||
aws_access_key_id="os.environ/AWS_BEDROCK_ONLY_ACCESS_KEY_ID",
|
||||
aws_secret_access_key="os.environ/AWS_BEDROCK_ONLY_SECRET_ACCESS_KEY",
|
||||
aws_region_name="os.environ/AWS_REGION",
|
||||
s3_region_name="os.environ/AWS_REGION",
|
||||
s3_bucket_name="os.environ/AWS_BATCH_S3_BUCKET",
|
||||
s3_access_key_id="os.environ/AWS_S3_ONLY_ACCESS_KEY_ID",
|
||||
s3_secret_access_key="os.environ/AWS_S3_ONLY_SECRET_ACCESS_KEY",
|
||||
aws_batch_role_arn="os.environ/AWS_BATCH_ROLE_ARN",
|
||||
)
|
||||
|
||||
|
||||
class TestBedrockBatchSplitS3Credentials:
|
||||
"""Bedrock batch deployment whose aws_* identity cannot touch the bucket.
|
||||
|
||||
AWS_BEDROCK_ONLY_* is an IAM user with no S3 rights on AWS_BATCH_S3_BUCKET;
|
||||
AWS_S3_ONLY_* is an IAM user with object rights on that bucket only. Every
|
||||
S3 call the proxy signs (PutObject on upload, GetObject on content,
|
||||
DeleteObject on delete) must use the s3_* pair, otherwise S3 answers 403.
|
||||
"""
|
||||
|
||||
@pytest.mark.covers(
|
||||
"llm.files.bedrock.split_s3_credentials.nonstream.works",
|
||||
exercised_on=["files"],
|
||||
)
|
||||
def test_file_lifecycle_signs_s3_with_s3_credentials(
|
||||
self, client: BatchClient, resources: ResourceManager
|
||||
) -> None:
|
||||
model_name = batch_model_name("bedrock-split-s3-batch")
|
||||
model_id = client.create_model(model_name, _split_s3_identity_params())
|
||||
resources.defer(lambda: client.delete_model(model_id))
|
||||
key = resources.key()
|
||||
|
||||
uploaded = client.upload_file(
|
||||
content=render_jsonl(ASSUME_ROLE_RAW_MODEL),
|
||||
form=FileUploadForm(purpose="batch", target_model_names=model_name),
|
||||
key=key,
|
||||
)
|
||||
assert isinstance(uploaded, Success), (
|
||||
f"upload must sign the S3 PutObject with s3_access_key_id, got {uploaded!r}"
|
||||
)
|
||||
file = uploaded.data
|
||||
resources.defer(lambda: cleanup_file(client, file.id, key=key))
|
||||
assert_file_object(file, provider="bedrock")
|
||||
|
||||
downloaded = client.proxy.transport.download(
|
||||
f"/v1/files/{file.id}/content",
|
||||
headers=client.proxy.transport.bearer(key),
|
||||
)
|
||||
assert downloaded.status_code == 200, (
|
||||
f"content must sign the S3 GetObject with s3_access_key_id, "
|
||||
f"got {downloaded.status_code}: {downloaded.body[:300]}"
|
||||
)
|
||||
assert all(json.loads(line) for line in downloaded.body.strip().splitlines()), (
|
||||
f"content download returned non-JSONL body: {downloaded.body[:200]}"
|
||||
)
|
||||
|
||||
deleted = client.delete_file(file.id, key=key)
|
||||
assert isinstance(deleted, Success), (
|
||||
f"delete must sign the S3 DeleteObject with s3_access_key_id, got {deleted!r}"
|
||||
)
|
||||
assert deleted.data.id == file.id, f"delete confirmed a different file: {deleted.data!r}"
|
||||
|
||||
|
||||
GOVCLOUD_REGION: Final = "us-gov-west-1"
|
||||
GOVCLOUD_RAW_MODEL: Final = "bedrock/amazon.nova-lite-v1:0"
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
- {id: llm.files.vertex.upload.nonstream.works, module: llm, tier: P0, subject_endpoint: files, route: vertex, capability: basic, streaming: nonstream, assertions: [works], source: "batches/capabilities.py:52", rationale: "Vertex file upload to GCS"}
|
||||
- {id: llm.files.bedrock.upload.nonstream.works, module: llm, tier: P0, subject_endpoint: files, route: bedrock_converse, capability: basic, streaming: nonstream, assertions: [works], source: "batches/capabilities.py:59", rationale: "Bedrock file upload to S3"}
|
||||
- {id: llm.files.bedrock.govcloud_partition.nonstream.works, module: llm, tier: P0, subject_endpoint: files, route: bedrock_converse, capability: govcloud_partition, streaming: nonstream, assertions: [works], source: "test_batches_e2e.py", rationale: "Bedrock file upload to an S3 bucket in the us-gov-west-1 partition"}
|
||||
- {id: llm.files.bedrock.split_s3_credentials.nonstream.works, module: llm, tier: P0, subject_endpoint: files, route: bedrock_converse, capability: split_s3_credentials, streaming: nonstream, assertions: [works], source: "test_batches_e2e.py / LIT-8297", rationale: "Bedrock file upload, content and delete sign S3 with s3_access_key_id / s3_secret_access_key when they differ from the aws_* identity"}
|
||||
- {id: llm.files.gemini.upload.nonstream.works, module: llm, tier: P1, subject_endpoint: files, route: gemini, capability: basic, streaming: nonstream, assertions: [works], source: "test_batches_e2e.py", rationale: "Gemini Files API upload via proxy"}
|
||||
- {id: llm.files.hosted_vllm.upload.nonstream.works, module: llm, tier: P1, subject_endpoint: files, route: hosted_vllm, capability: basic, streaming: nonstream, assertions: [works], source: "test_batches_e2e.py", rationale: "hosted_vllm OpenAI-compatible file upload"}
|
||||
- {id: llm.files.openai.require_managed_files_upload.nonstream.works, module: llm, tier: P1, subject_endpoint: files, route: openai, capability: input_validation, streaming: nonstream, assertions: [works], source: "test_managed_files_enforcement_e2e.py / LIT-5902", rationale: "With require_managed_files enabled, an upload without target_model_names and an upload carrying a model param are both rejected 400; runs only in the sequential managed-files stack phase (E2E_MANAGED_FILES_STACK)"}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ LlmCapability = Literal[
|
|||
"batch_deployment",
|
||||
"count_tokens",
|
||||
"govcloud_partition",
|
||||
"split_s3_credentials",
|
||||
"input_validation",
|
||||
"long_context_1m",
|
||||
"mid_conversation_system",
|
||||
|
|
|
|||
|
|
@ -67,6 +67,15 @@ class TestGetLitellmParamsKwargsExtraction:
|
|||
assert "s3_endpoint_url" not in result_without_s3_kwargs
|
||||
assert "s3_region_name" not in result_without_s3_kwargs
|
||||
|
||||
def test_s3_credential_kwargs_are_forwarded_for_s3_signing(self):
|
||||
result = get_litellm_params(s3_access_key_id="s3-key", s3_secret_access_key="s3-secret")
|
||||
assert result["s3_access_key_id"] == "s3-key"
|
||||
assert result["s3_secret_access_key"] == "s3-secret"
|
||||
|
||||
result_without_s3_kwargs = get_litellm_params()
|
||||
assert "s3_access_key_id" not in result_without_s3_kwargs
|
||||
assert "s3_secret_access_key" not in result_without_s3_kwargs
|
||||
|
||||
def test_subset_of_kwargs_only_includes_provided(self):
|
||||
"""Only provided kwargs appear, others remain absent."""
|
||||
result = get_litellm_params(azure_ad_token="token123")
|
||||
|
|
|
|||
|
|
@ -926,3 +926,31 @@ def test_every_bedrock_config_get_error_class_keeps_provider_headers(config):
|
|||
|
||||
def test_bedrock_get_error_class_audit_covers_every_surface():
|
||||
assert len(_bedrock_configs_with_get_error_class()) >= 30
|
||||
|
||||
|
||||
def test_s3_static_key_pair_returns_the_pair_when_both_keys_are_set():
|
||||
from litellm.llms.bedrock.common_utils import s3_static_key_pair
|
||||
|
||||
assert s3_static_key_pair(
|
||||
{
|
||||
"aws_access_key_id": "bedrock-key",
|
||||
"aws_secret_access_key": "bedrock-secret",
|
||||
"s3_access_key_id": "s3-key",
|
||||
"s3_secret_access_key": "s3-secret",
|
||||
}
|
||||
) == ("s3-key", "s3-secret")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"partial_s3_pair",
|
||||
[
|
||||
{},
|
||||
{"s3_access_key_id": "s3-key"},
|
||||
{"s3_secret_access_key": "s3-secret"},
|
||||
{"s3_access_key_id": "", "s3_secret_access_key": ""},
|
||||
],
|
||||
)
|
||||
def test_s3_static_key_pair_is_none_without_a_full_pair(partial_s3_pair):
|
||||
from litellm.llms.bedrock.common_utils import s3_static_key_pair
|
||||
|
||||
assert s3_static_key_pair({"aws_access_key_id": "bedrock-key", **partial_s3_pair}) is None
|
||||
|
|
|
|||
|
|
@ -270,3 +270,40 @@ async def test_afile_content_assumes_role_with_external_id(monkeypatch):
|
|||
assert s3_client_kwargs["aws_access_key_id"] == "ASIAFILESDOWNLOADROLE"
|
||||
assert s3_client_kwargs["aws_session_token"] == "assumed-session-token"
|
||||
assert response.content == b'{"custom_id": "req-1"}'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_afile_content_builds_the_s3_client_with_the_s3_pair_when_it_differs_from_the_aws_identity():
|
||||
import boto3
|
||||
|
||||
class FakeS3Body:
|
||||
def read(self):
|
||||
return b'{"custom_id": "req-1"}'
|
||||
|
||||
class FakeS3Client:
|
||||
def get_object(self, Bucket, Key):
|
||||
return {"Body": FakeS3Body()}
|
||||
|
||||
optional_params = {
|
||||
"_litellm_internal_model_credentials": MappingProxyType({"s3_bucket_name": "safe-bucket"}),
|
||||
"aws_region_name": "us-east-1",
|
||||
"aws_access_key_id": "AKIABEDROCKONLY",
|
||||
"aws_secret_access_key": "bedrock-only-secret",
|
||||
"aws_session_token": "bedrock-only-token",
|
||||
"s3_access_key_id": "AKIAS3ONLY",
|
||||
"s3_secret_access_key": "s3-only-secret",
|
||||
}
|
||||
|
||||
with patch.object(boto3, "client", return_value=FakeS3Client()) as mock_boto3_client:
|
||||
response = await BedrockFilesHandler().afile_content(
|
||||
file_content_request={"file_id": "s3://safe-bucket/litellm-bedrock-files-model-id-abc.jsonl"},
|
||||
optional_params=optional_params,
|
||||
timeout=10.0,
|
||||
max_retries=None,
|
||||
)
|
||||
|
||||
s3_client_kwargs = mock_boto3_client.call_args.kwargs
|
||||
assert s3_client_kwargs["aws_access_key_id"] == "AKIAS3ONLY"
|
||||
assert s3_client_kwargs["aws_secret_access_key"] == "s3-only-secret"
|
||||
assert s3_client_kwargs["aws_session_token"] is None, "the aws_* session token belongs to the Bedrock identity"
|
||||
assert response.content == b'{"custom_id": "req-1"}'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json
|
|||
import os
|
||||
from collections.abc import Mapping
|
||||
from contextlib import AsyncExitStack, closing
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
from unittest.mock import MagicMock
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
|
@ -3789,3 +3790,82 @@ class TestBedrockFileListTransformation:
|
|||
|
||||
assert denied.value.status_code == 403
|
||||
assert "AccessDenied" in denied.value.message
|
||||
|
||||
|
||||
_SPLIT_IDENTITY_PARAMS: Final = {
|
||||
"aws_region_name": "us-east-1",
|
||||
"aws_access_key_id": "AKIABEDROCKONLY",
|
||||
"aws_secret_access_key": "bedrock-only-secret",
|
||||
"s3_access_key_id": "AKIAS3ONLY",
|
||||
"s3_secret_access_key": "s3-only-secret",
|
||||
"s3_bucket_name": "safe-bucket",
|
||||
}
|
||||
|
||||
|
||||
def _authorization(headers: Mapping[str, str]) -> str:
|
||||
return {key.lower(): value for key, value in headers.items()}["authorization"]
|
||||
|
||||
|
||||
def test_sign_s3_request_uses_the_s3_pair_when_it_differs_from_the_aws_identity():
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
signed_headers, _signed_body = BedrockFilesConfig()._sign_s3_request(
|
||||
content='{"custom_id": "req-1"}',
|
||||
api_base="https://s3.us-east-1.amazonaws.com/safe-bucket/litellm-bedrock-files-model-id-abc.jsonl",
|
||||
optional_params=dict(_SPLIT_IDENTITY_PARAMS),
|
||||
)
|
||||
|
||||
assert _authorization(signed_headers).startswith("AWS4-HMAC-SHA256 Credential=AKIAS3ONLY/"), (
|
||||
"the S3 PutObject must be signed by s3_access_key_id, not the Bedrock aws_access_key_id"
|
||||
)
|
||||
|
||||
|
||||
def test_sign_s3_request_with_the_s3_pair_ignores_ambient_aws_session_token_role_and_profile(monkeypatch):
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig
|
||||
|
||||
monkeypatch.setenv("AWS_SESSION_TOKEN", "pod-token")
|
||||
monkeypatch.setenv("AWS_ROLE_NAME", "arn:aws:iam::123456789012:role/pod")
|
||||
monkeypatch.setenv("AWS_PROFILE_NAME", "pod-profile")
|
||||
signed_headers, _signed_body = BedrockFilesConfig()._sign_s3_request(
|
||||
content='{"custom_id": "req-1"}',
|
||||
api_base="https://s3.us-east-1.amazonaws.com/safe-bucket/litellm-bedrock-files-model-id-abc.jsonl",
|
||||
optional_params=dict(_SPLIT_IDENTITY_PARAMS),
|
||||
)
|
||||
|
||||
lowered: Final = {key.lower(): value for key, value in signed_headers.items()}
|
||||
assert lowered["authorization"].startswith("AWS4-HMAC-SHA256 Credential=AKIAS3ONLY/")
|
||||
assert "x-amz-security-token" not in lowered, "an ambient AWS_SESSION_TOKEN must not be mixed into the s3_* pair"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["GET", "DELETE"])
|
||||
def test_sign_s3_request_without_body_uses_the_s3_pair_when_it_differs_from_the_aws_identity(method):
|
||||
from litellm.llms.bedrock.files.transformation import BedrockFilesConfig, _BedrockS3RequestParams
|
||||
|
||||
signed_headers = BedrockFilesConfig()._sign_s3_request_without_body(
|
||||
method=method,
|
||||
api_base="https://s3.us-east-1.amazonaws.com/safe-bucket/litellm-bedrock-files-model-id-abc.jsonl",
|
||||
aws_region_name="us-east-1",
|
||||
request_params=_BedrockS3RequestParams.model_validate(_SPLIT_IDENTITY_PARAMS),
|
||||
)
|
||||
|
||||
assert _authorization(signed_headers).startswith("AWS4-HMAC-SHA256 Credential=AKIAS3ONLY/"), (
|
||||
f"the S3 {method} must be signed by s3_access_key_id, not the Bedrock aws_access_key_id"
|
||||
)
|
||||
|
||||
|
||||
def test_transform_file_content_request_signs_with_the_s3_pair_from_litellm_params():
|
||||
from litellm.llms.bedrock.files.transformation import S3_SIGNED_REQUEST_HEADERS_PARAM, BedrockFilesConfig
|
||||
|
||||
litellm_params = {
|
||||
**_SPLIT_IDENTITY_PARAMS,
|
||||
"_litellm_internal_model_credentials": MappingProxyType({"s3_bucket_name": "safe-bucket"}),
|
||||
}
|
||||
BedrockFilesConfig().transform_file_content_request(
|
||||
file_content_request={"file_id": "s3://safe-bucket/litellm-bedrock-files-model-id-abc.jsonl"},
|
||||
optional_params={},
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
assert _authorization(litellm_params[S3_SIGNED_REQUEST_HEADERS_PARAM]).startswith(
|
||||
"AWS4-HMAC-SHA256 Credential=AKIAS3ONLY/"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue