From c279c31c6d2a09087b8f26d6453eb2864f8f784d Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 13 May 2026 14:13:15 +0800 Subject: [PATCH 1/2] fix(storage): support AWS_S3_ADDRESSING_STYLE env var for S3 virtual/path addressing --- apps/api/plane/settings/storage.py | 20 +++- .../plane/tests/unit/settings/test_storage.py | 94 +++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) diff --git a/apps/api/plane/settings/storage.py b/apps/api/plane/settings/storage.py index e4a978bd2b1..a15bfdb97e9 100644 --- a/apps/api/plane/settings/storage.py +++ b/apps/api/plane/settings/storage.py @@ -8,6 +8,7 @@ # Third party imports import boto3 +from botocore.config import Config from botocore.exceptions import ClientError from urllib.parse import quote @@ -36,6 +37,21 @@ def __init__(self, request=None): # Use the SIGNED_URL_EXPIRATION environment variable for the expiration time (default: 3600 seconds) self.signed_url_expiration = int(os.environ.get("SIGNED_URL_EXPIRATION", "3600")) + # S3 addressing style: 'auto', 'virtual', or 'path' (default: 'auto') + # virtual = virtual-hosted style (e.g., https://bucket.s3.amazonaws.com) + # path = path style (e.g., https://s3.amazonaws.com/bucket) + # auto = let botocore decide based on bucket name + addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").lower() + + # Create boto3 Config with addressing style if explicitly set + if addressing_style in ("virtual", "path"): + boto_config = Config( + signature_version="s3v4", + s3={"addressing_style": addressing_style}, + ) + else: + boto_config = Config(signature_version="s3v4") + if os.environ.get("USE_MINIO") == "1": # Determine protocol based on environment variable if os.environ.get("MINIO_ENDPOINT_SSL") == "1": @@ -49,7 +65,7 @@ def __init__(self, request=None): aws_secret_access_key=self.aws_secret_access_key, region_name=self.aws_region, endpoint_url=(f"{endpoint_protocol}://{request.get_host()}" if request else self.aws_s3_endpoint_url), - config=boto3.session.Config(signature_version="s3v4"), + config=boto_config, ) else: # Create an S3 client @@ -59,7 +75,7 @@ def __init__(self, request=None): aws_secret_access_key=self.aws_secret_access_key, region_name=self.aws_region, endpoint_url=self.aws_s3_endpoint_url, - config=boto3.session.Config(signature_version="s3v4"), + config=boto_config, ) def generate_presigned_post(self, object_name, file_type, file_size, expiration=None): diff --git a/apps/api/plane/tests/unit/settings/test_storage.py b/apps/api/plane/tests/unit/settings/test_storage.py index 00856aeecb6..e3c3983cf04 100644 --- a/apps/api/plane/tests/unit/settings/test_storage.py +++ b/apps/api/plane/tests/unit/settings/test_storage.py @@ -204,3 +204,97 @@ def test_explicit_expiration_overrides_default(self, mock_boto3): mock_s3_client.generate_presigned_url.assert_called_once() call_kwargs = mock_s3_client.generate_presigned_url.call_args[1] assert call_kwargs["ExpiresIn"] == 120 + + +@pytest.mark.unit +class TestS3StorageAddressingStyle: + """Test the S3 addressing style configuration via AWS_S3_ADDRESSING_STYLE""" + + @patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-key", + "AWS_SECRET_ACCESS_KEY": "test-secret", + "AWS_S3_BUCKET_NAME": "test-bucket", + "AWS_REGION": "us-east-1", + "AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com", + "AWS_S3_ADDRESSING_STYLE": "virtual", + }, + clear=True, + ) + @patch("plane.settings.storage.boto3") + def test_virtual_addressing_style(self, mock_boto3): + """Test that virtual addressing style is configured via botocore Config""" + mock_boto3.client.return_value = Mock() + + storage = S3Storage() + + call_kwargs = mock_boto3.client.call_args[1] + assert call_kwargs["config"].s3["addressing_style"] == "virtual" + + @patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-key", + "AWS_SECRET_ACCESS_KEY": "test-secret", + "AWS_S3_BUCKET_NAME": "test-bucket", + "AWS_REGION": "us-east-1", + "AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com", + "AWS_S3_ADDRESSING_STYLE": "path", + }, + clear=True, + ) + @patch("plane.settings.storage.boto3") + def test_path_addressing_style(self, mock_boto3): + """Test that path addressing style is configured via botocore Config""" + mock_boto3.client.return_value = Mock() + + storage = S3Storage() + + call_kwargs = mock_boto3.client.call_args[1] + assert call_kwargs["config"].s3["addressing_style"] == "path" + + @patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-key", + "AWS_SECRET_ACCESS_KEY": "test-secret", + "AWS_S3_BUCKET_NAME": "test-bucket", + "AWS_REGION": "us-east-1", + "AWS_S3_ENDPOINT_URL": "https://s3.amazonaws.com", + }, + clear=True, + ) + @patch("plane.settings.storage.boto3") + def test_auto_addressing_style_by_default(self, mock_boto3): + """Test that auto addressing style is used by default (no s3 config in botocore Config)""" + mock_boto3.client.return_value = Mock() + + storage = S3Storage() + + call_kwargs = mock_boto3.client.call_args[1] + # When addressing_style is 'auto' or not set, botocore Config should not have s3 dict + assert "s3" not in call_kwargs["config"]._user_provided_options + + @patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-key", + "AWS_SECRET_ACCESS_KEY": "test-secret", + "AWS_S3_BUCKET_NAME": "test-bucket", + "AWS_REGION": "us-east-1", + "AWS_S3_ENDPOINT_URL": "https://nyc3.digitaloceanspaces.com", + "AWS_S3_ADDRESSING_STYLE": "virtual", + }, + clear=True, + ) + @patch("plane.settings.storage.boto3") + def test_virtual_style_with_digitalocean_spaces(self, mock_boto3): + """Test virtual addressing style works with DigitalOcean Spaces""" + mock_boto3.client.return_value = Mock() + + storage = S3Storage() + + call_kwargs = mock_boto3.client.call_args[1] + assert call_kwargs["config"].s3["addressing_style"] == "virtual" + assert call_kwargs["endpoint_url"] == "https://nyc3.digitaloceanspaces.com" From c97fc104611d25f80e61d6e85a212d7af8e6b8bd Mon Sep 17 00:00:00 2001 From: Cody Date: Wed, 13 May 2026 20:47:17 +0800 Subject: [PATCH 2/2] fix(storage): normalize AWS_S3_ADDRESSING_STYLE with strip().lower() --- apps/api/plane/settings/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/plane/settings/storage.py b/apps/api/plane/settings/storage.py index a15bfdb97e9..bc73caa3739 100644 --- a/apps/api/plane/settings/storage.py +++ b/apps/api/plane/settings/storage.py @@ -41,7 +41,7 @@ def __init__(self, request=None): # virtual = virtual-hosted style (e.g., https://bucket.s3.amazonaws.com) # path = path style (e.g., https://s3.amazonaws.com/bucket) # auto = let botocore decide based on bucket name - addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").lower() + addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").strip().lower() # Create boto3 Config with addressing style if explicitly set if addressing_style in ("virtual", "path"):