Skip to content

fix(api): drop duplicate {bucket}/{key} conditions from presigned-POST policy#9005

Open
volodchenkov wants to merge 3 commits intomakeplane:previewfrom
volodchenkov:fix/presigned-post-policy-too-large
Open

fix(api): drop duplicate {bucket}/{key} conditions from presigned-POST policy#9005
volodchenkov wants to merge 3 commits intomakeplane:previewfrom
volodchenkov:fix/presigned-post-policy-too-large

Conversation

@volodchenkov
Copy link
Copy Markdown

@volodchenkov volodchenkov commented May 3, 2026

Description

boto3.generate_presigned_post() automatically adds {"bucket": ...} and
{"key": ...} conditions to the generated policy whenever Bucket= and
Key= are passed as named arguments — see the [boto3 docs][1].
S3Storage.generate_presigned_post adds them again by hand in
conditions=, so the policy carries each condition twice and is ~80 bytes
larger than it needs to be.

AWS S3 silently accepts the over-sized policy. Some S3-compatible backends
do not — at least one mainstream vendor enforces a hard cap of 1024
bytes
on the policy form field and rejects the upload with HTTP 400:

<Error>
  <Code>MaxMessageLengthExceeded</Code>
  <Attribute>policy</Attribute>
  <MaxSizeAllowed>1024</MaxSizeAllowed>
  <ProposedSize>1160</ProposedSize>
</Error>

The duplication makes attachment upload completely broken on those
backends.

After this fix, the resulting policy drops from ~1160 to ~564 bytes for a
typical attachment, well under every documented per-backend limit.
Behaviour on AWS S3 is unchanged (boto3 still injects the same conditions
on its own).

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Test Scenarios

  • New unit test test_generate_presigned_post_does_not_duplicate_bucket_or_key
    asserts that Conditions= no longer contains hand-rolled
    {"bucket": ...} / {"key": ...} entries.
  • Manually verified end-to-end against a self-hosted Plane backed by an
    S3-compatible storage that previously rejected uploads with
    MaxMessageLengthExceeded: file attachments now upload successfully.
  • AWS S3 behaviour spot-checked via boto3==1.34.96: produced policy still
    contains the required bucket/key conditions (boto3 injects them) and
    presigned uploads succeed.

References

None — no prior issue. Bug discovered while running self-hosted Plane on
an S3-compatible storage with strict policy size limits.

Summary by CodeRabbit

  • Bug Fixes

    • Presigned POST generation no longer duplicates bucket/key constraints or manually adds filename "starts-with" conditions, preventing oversized policies and upload failures on strict S3-compatible backends.
  • Tests

    • Added unit tests to verify presigned POSTs omit duplicate bucket/key entries and do not manually inject filename-placeholder conditions.

…T policy

`boto3.generate_presigned_post()` automatically adds `{"bucket": ...}` and
`{"key": ...}` conditions to the policy whenever `Bucket=` and `Key=` are
passed as named arguments. The current code adds them by hand in
`conditions=` *as well*, so every generated policy carries each condition
twice and is roughly 80 bytes larger than it needs to be.

AWS S3 silently accepts the over-sized policy, but stricter S3-compatible
backends do not — some enforce a `policy` field limit as low as 1024 bytes
and reject the upload with HTTP 400:

    <Error>
      <Code>MaxMessageLengthExceeded</Code>
      <Attribute>policy</Attribute>
      <MaxSizeAllowed>1024</MaxSizeAllowed>
      <ProposedSize>1160</ProposedSize>
    </Error>

The duplication makes attachment upload unusable on those backends.

This change removes the two hand-rolled conditions and lets boto3 add them
itself. Resulting policy size drops from ~1160 to ~564 bytes for a typical
attachment, well under every documented per-backend limit. Behaviour on
AWS S3 is unchanged.

Adds a regression test asserting that `Conditions=` no longer contains
hand-rolled `bucket`/`key` entries.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 3, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 326c7a53-efc4-40b6-875d-b27c7839438b

📥 Commits

Reviewing files that changed from the base of the PR and between fde7bae and ee3d16b.

📒 Files selected for processing (2)
  • apps/api/plane/settings/storage.py
  • apps/api/plane/tests/unit/settings/test_storage.py

📝 Walkthrough

Walkthrough

Remove manually added S3 bucket/key and ${filename} "starts-with" constraints from presigned POST policy generation; rely on boto3 to inject bucket/key when Bucket/Key are passed. Add unit tests asserting no duplicated bucket/key nor manual starts-with for ${filename}.

Changes

S3 Presigned POST Policy Fix

Layer / File(s) Summary
Core Implementation
apps/api/plane/settings/storage.py
S3Storage.generate_presigned_post no longer inserts manual {"bucket": ...} or {"key": ...} constraints, nor a manual fields["key"] or a starts-with condition for ${filename}; conditions now include only content-length-range and {"Content-Type": ...} and relies on boto3 to add bucket/key policy constraints when Bucket and Key are provided.
Inline Documentation
apps/api/plane/settings/storage.py
Added comments explaining that boto3.generate_presigned_post auto-injects bucket/key constraints (including special ${filename} handling) and that duplicating them is unnecessary.
Tests
apps/api/plane/tests/unit/settings/test_storage.py
Added test_generate_presigned_post_does_not_duplicate_bucket_or_key and test_generate_presigned_post_no_manual_starts_with_for_filename_placeholder to assert Bucket/Key are passed as named kwargs, and that Conditions and returned Fields do not contain hand-rolled {"bucket"}, {"key"}, or manual "starts-with" entries.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through policies, tidy and neat,
Removed the echoes of bucket and key.
boto3 hums and handles the feat,
Tests hop in place—no duplicate spree.
A carrot for order, a thump of glee. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: removing duplicate bucket/key conditions from the presigned-POST policy, which is the core fix addressed in this PR.
Description check ✅ Passed The PR description comprehensively covers all required template sections: detailed Description, Type of Change (Bug fix), Test Scenarios, and References, with clear technical context and verification details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/api/plane/settings/storage.py (1)

71-86: ⚡ Quick win

Drop the remaining manual key field assignment, or verify it is still required.

Botocore’s generate_presigned_post docs say key-related fields and conditions should not be included in Fields or Conditions; this change removes the duplicate bucket/key conditions, but fields["key"] is still injected later in the non-${filename} path, which looks redundant and may keep the implementation off the documented path. (botocore.amazonaws.com)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/plane/settings/storage.py` around lines 71 - 86, The code still
injects a manual key into fields when object_name does not start with
"${filename}" which contradicts botocore's generate_presigned_post guidance;
remove the fields["key"] = object_name assignment in the else branch (or, if you
have a special non-standard S3 backend that requires it, gate that assignment
behind an explicit configuration flag) so that only conditions and fields
generated by boto3/botocore are used; update any tests that assumed the manual
key to rely on the presigned post behavior from generate_presigned_post (refer
to object_name, fields, conditions, and generate_presigned_post in the
surrounding code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/api/plane/settings/storage.py`:
- Around line 71-86: The code still injects a manual key into fields when
object_name does not start with "${filename}" which contradicts botocore's
generate_presigned_post guidance; remove the fields["key"] = object_name
assignment in the else branch (or, if you have a special non-standard S3 backend
that requires it, gate that assignment behind an explicit configuration flag) so
that only conditions and fields generated by boto3/botocore are used; update any
tests that assumed the manual key to rely on the presigned post behavior from
generate_presigned_post (refer to object_name, fields, conditions, and
generate_presigned_post in the surrounding code).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c93bbce-3518-485b-96c3-2ba2a7c9190d

📥 Commits

Reviewing files that changed from the base of the PR and between a62fe8a and 69f50ac.

📒 Files selected for processing (2)
  • apps/api/plane/settings/storage.py
  • apps/api/plane/tests/unit/settings/test_storage.py

Per CodeRabbit review on PR makeplane#9005: with Bucket=/Key= passed to
boto3.generate_presigned_post() below, boto3 already adds the `key` entry
to the returned `fields[]` itself — no need to set it by hand. Verified
empirically: same returned `fields`, same policy size, same signature
either way.

The `${filename}` branch still needs its `starts-with` condition; boto3
cannot derive that from a literal Key= value. Comment updated to make
that clear.

Test asserts `Conditions[]` and `Fields[]` are both free of hand-rolled
`key` entries.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/api/plane/tests/unit/settings/test_storage.py (1)

119-162: ⚡ Quick win

New test is well-structured but doesn't cover the ${filename} upload path.

The test correctly validates that the exact-key path ("test-object") produces no hand-rolled bucket/key conditions. However, there's no test for the object_name.startswith("${filename}") branch. Given the finding above — that this branch may produce a duplicate starts-with condition that botocore already injects — a companion test would:

  1. Catch the redundant condition today, and
  2. Serve as a regression guard if the branch is removed.
➕ Suggested additional test for the `${filename}` 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",
    },
    clear=True,
)
`@patch`("plane.settings.storage.boto3")
def test_generate_presigned_post_filename_placeholder_no_duplicate_starts_with(self, mock_boto3):
    """For ${filename} keys boto3/botocore auto-injects the starts-with condition.
    No manual duplicate should appear in Conditions."""
    mock_s3_client = Mock()
    mock_s3_client.generate_presigned_post.return_value = {
        "url": "https://test-url.com",
        "fields": {},
    }
    mock_boto3.client.return_value = mock_s3_client
    storage = S3Storage()

    storage.generate_presigned_post("${filename}", "image/png", 1024)

    kw = mock_s3_client.generate_presigned_post.call_args[1]
    assert kw["Key"] == "${filename}"
    starts_with_key_conditions = [
        c for c in kw["Conditions"]
        if isinstance(c, list) and len(c) == 3 and c[0] == "starts-with" and c[1] == "$key"
    ]
    # botocore will inject one; we must not inject a second.
    assert len(starts_with_key_conditions) <= 1, (
        f"duplicate starts-with $key conditions: {starts_with_key_conditions}"
    )
🤖 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/tests/unit/settings/test_storage.py` around lines 119 - 162,
Add a new unit test for the object_name.startswith("${filename}") branch of
S3Storage.generate_presigned_post to ensure we don't inject a duplicate
starts-with condition; create a test (e.g.
test_generate_presigned_post_filename_placeholder_no_duplicate_starts_with) that
patches os.environ and boto3 like the existing test, calls
storage.generate_presigned_post("${filename}", "image/png", 1024), captures
mock_s3_client.generate_presigned_post.call_args[1], asserts kw["Key"] ==
"${filename}", collects any ["starts-with", "$key", ...] conditions from
kw["Conditions"] and asserts there is at most one, and also assert kw["Fields"]
does not contain a hand-rolled "key" field.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@apps/api/plane/settings/storage.py`:
- Around line 82-88: Remove the incorrect startswith check and its associated
comment in the presigned-post construction (the block that inspects object_name
and appends ["starts-with", "$key", ...]); botocore/boto3's
generate_presigned_post already handles keys ending with '${filename}' (it
derives the starts-with condition via an endswith check), so delete the if
object_name.startswith("${filename}"): ... conditions.append(...) block and
replace the comment with a brief note that botocore generates the required
starts-with condition for keys containing the '${filename}' placeholder (no
manual injection needed). Ensure references to object_name and the conditions
list remain otherwise unchanged.

---

Nitpick comments:
In `@apps/api/plane/tests/unit/settings/test_storage.py`:
- Around line 119-162: Add a new unit test for the
object_name.startswith("${filename}") branch of
S3Storage.generate_presigned_post to ensure we don't inject a duplicate
starts-with condition; create a test (e.g.
test_generate_presigned_post_filename_placeholder_no_duplicate_starts_with) that
patches os.environ and boto3 like the existing test, calls
storage.generate_presigned_post("${filename}", "image/png", 1024), captures
mock_s3_client.generate_presigned_post.call_args[1], asserts kw["Key"] ==
"${filename}", collects any ["starts-with", "$key", ...] conditions from
kw["Conditions"] and asserts there is at most one, and also assert kw["Fields"]
does not contain a hand-rolled "key" field.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43e97853-67ce-403f-80a6-4311d95bf68e

📥 Commits

Reviewing files that changed from the base of the PR and between 69f50ac and fde7bae.

📒 Files selected for processing (2)
  • apps/api/plane/settings/storage.py
  • apps/api/plane/tests/unit/settings/test_storage.py

Comment thread apps/api/plane/settings/storage.py Outdated
…itself

Per second CodeRabbit review pass on PR makeplane#9005:

1. boto3.generate_presigned_post auto-injects ["starts-with", "$key", <prefix>]
   into the policy whenever the supplied Key ends with the literal
   "${filename}" placeholder. Verified empirically against a live S3
   endpoint:

       Key="prefix/${filename}", Conditions=[…no starts-with…]
       → returned policy contains ["starts-with", "$key", "prefix/"]

   Adding the same condition by hand produced two identical
   ["starts-with", "$key", "prefix/"] entries.

2. The pre-existing branch was also logically broken: it tested
   `object_name.startswith("${filename}")`, while botocore's actual
   detection is `endswith()`. For the typical `prefix/${filename}` case
   `startswith()` is False — the branch never fired. For the
   degenerate `${filename}` (no prefix) case it produced
   `["starts-with", "$key", ""]` (matches any key), which is
   meaningless.

3. No call site in this codebase passes an `object_name` containing
   `${filename}` — every caller in `apps/api/plane/{api,app,space}/views/`
   resolves the asset key to a concrete UUID string before calling
   `generate_presigned_post`. The branch was dead.

Removes the conditional and the "still need a starts-with" comment.
Adds a companion regression test asserting the wrapper passes Key= through
unchanged and does not inject any starts-with into Conditions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants