Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions apps/api/plane/settings/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# Third party imports
import boto3
from botocore.config import Config
from botocore.exceptions import ClientError
from urllib.parse import quote

Expand Down Expand Up @@ -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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize and validate AWS_S3_ADDRESSING_STYLE to avoid silent misconfiguration.

A value like " Virtual " or typo currently falls through to auto without any signal, which can hide config mistakes and make debugging provider-specific failures harder. Normalize with .strip().lower() and explicitly guard allowed values.

💡 Proposed fix
-        addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").lower()
+        addressing_style = os.environ.get("AWS_S3_ADDRESSING_STYLE", "auto").strip().lower()
+        if addressing_style not in {"auto", "virtual", "path"}:
+            raise ValueError(
+                "AWS_S3_ADDRESSING_STYLE must be one of: auto, virtual, path"
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/settings/storage.py` around lines 44 - 53, The code reads
AWS_S3_ADDRESSING_STYLE but doesn't normalize/validate it, so values like "
Virtual " or typos silently fall back to auto; update the retrieval of
AWS_S3_ADDRESSING_STYLE to use .strip().lower(), validate against the allowed
set ("virtual","path","auto"), and if the value is invalid log or raise a clear
error before constructing boto_config; then use the normalized addressing_style
when building the Config (see addressing_style, AWS_S3_ADDRESSING_STYLE,
boto_config, Config).


if os.environ.get("USE_MINIO") == "1":
# Determine protocol based on environment variable
if os.environ.get("MINIO_ENDPOINT_SSL") == "1":
Expand All @@ -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
Expand All @@ -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):
Expand Down
94 changes: 94 additions & 0 deletions apps/api/plane/tests/unit/settings/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"