Skip to content

Revert "feat: add org logo upload and branding support"#1406

Open
NaveenCode wants to merge 9 commits into
mainfrom
feature/org-logo-upload
Open

Revert "feat: add org logo upload and branding support"#1406
NaveenCode wants to merge 9 commits into
mainfrom
feature/org-logo-upload

Conversation

@NaveenCode

@NaveenCode NaveenCode commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
  • Add logo_url, logo_s3_key, logo_filename fields to Org model (migration 0164)
  • Add org logo service with upload from file, upload from URL, and delete logic
  • Add org logo API endpoints: GET, POST /upload/, POST /url/, DELETE
  • Return logo_filename in OrgSchema and OrgLogoResponse for frontend tab state
  • Fix SVG logo blurriness in PDF by converting to high-res PNG via canvas before capture

Summary by CodeRabbit

  • New Features

    • Org logo management: upload (file or URL), retrieve (including a direct image proxy), and delete
    • Org logos displayed on public dashboards and reports
    • Improved SVG fetching and rasterization for better logo rendering in PDF exports
  • Chores

    • Optional env config for S3-hosted images and DB fields to store logo metadata
  • Tests

    • Added tests covering API behavior, service logic, S3 utilities, and success/error paths

- Add logo_url, logo_s3_key, logo_filename fields to Org model (migration 0164)
- Add org logo service with upload from file, upload from URL, and delete logic
- Add org logo API endpoints: GET, POST /upload/, POST /url/, DELETE
- Return logo_filename in OrgSchema and OrgLogoResponse for frontend tab state
- Fix SVG logo blurriness in PDF by converting to high-res PNG via canvas before capture
@NaveenCode NaveenCode self-assigned this Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@himanshudube97, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 11 seconds. Learn how PR review limits work.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d148f96d-5679-4379-9517-a41b7ee45458

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff4ce9 and ab551e4.

📒 Files selected for processing (3)
  • ddpui/api/org_logo_api.py
  • ddpui/core/org_logo/exceptions.py
  • ddpui/core/org_logo/org_logo_service.py

Walkthrough

Adds org logo support: env var, DB fields + migration, S3 upload/delete utilities and exceptions, OrgLogoService, REST endpoints at /api/org/logo/, public API exposure of logo URLs, PDF SVG rasterization, and tests.

Changes

Organization Logo Management

Layer / File(s) Summary
Data model, migration, env and schemas
.env.template, ddpui/models/org.py, ddpui/migrations/0164_org_logo.py, ddpui/schemas/org_schema.py
Adds S3_IMAGES env placeholder; adds logo_url, logo_s3_key, logo_filename to Org and migration; introduces OrgLogoResponse and OrgLogoUrlPayload schemas.
S3 utilities and domain exceptions
ddpui/core/org_logo/exceptions.py, ddpui/utils/s3_utils.py
Domain exceptions for logo flows; S3 helpers that validate MIME/size, resolve bucket/region, build public URL, upload org-scoped objects, and delete objects.
Logo business logic service
ddpui/core/org_logo/org_logo_service.py
OrgLogoService implements get, get_logo_bytes, upload-from-file (S3), upload-from-url (store external URL and cleanup), delete (with S3 cleanup), and an internal _replace_logo that updates DB fields.
REST API endpoints and routing
ddpui/api/org_logo_api.py, ddpui/routes.py
New org_logo_router with GET /, GET /proxy/, POST /upload/, POST /url/, DELETE / mounted at /api/org/logo/; endpoints enforce permissions and map service exceptions to HTTP 400/404/502/500 as appropriate.
Public API logo exposure
ddpui/api/public_api.py
PublicDashboardResponse gains org_logo_url and public dashboard/report responses populate it from the org's logo_url.
PDF export SVG-to-PNG rasterization
ddpui/core/reports/pdf_export_service.py
During Playwright PDF generation, downloads HTTPS SVGs, converts to base64 data URIs, rasterizes to PNG at 4× display size via canvas, and replaces <img> src before generating PDF.
Tests: API, service, and utils
ddpui/tests/api_tests/test_org_logo_api.py, ddpui/tests/core/org_logo/test_org_logo_service.py, ddpui/tests/utils/test_s3_utils.py
Adds API tests for exception→HTTP mappings, service tests for upload/delete and S3 interactions, and unit tests for S3 utilities and validation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • Ishankoradia
  • himanshudube97
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title states it reverts a feature, but the raw summary shows all changes are additions (new files, new fields, new endpoints) with no removals or reversions. Clarify whether this is truly a revert (should remove code) or a feature addition. If adding org logo support, use a title like 'feat: add org logo upload and branding support'.
Docstring Coverage ⚠️ Warning Docstring coverage is 34.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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
  • Commit unit tests in branch feature/org-logo-upload

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
ddpui/utils/s3_utils.py (1)

61-61: ⚡ Quick win

Remove unnecessary f-string prefix.

The string has no placeholders, so the f prefix is not needed.

♻️ Proposed fix
-        raise ValueError(f"File size exceeds the 5MB limit")
+        raise ValueError("File size exceeds the 5MB limit")
🤖 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 `@ddpui/utils/s3_utils.py` at line 61, The ValueError message uses an
unnecessary f-string prefix; replace raise ValueError(f"File size exceeds the
5MB limit") with a plain string (remove the leading f) so it becomes raise
ValueError("File size exceeds the 5MB limit") in the file size check (the raise
ValueError(...) statement in ddpui/utils/s3_utils.py).
ddpui/core/org_logo/org_logo_service.py (2)

95-100: ⚡ Quick win

Preserve exception chain when re-raising.

Use raise ... from e to maintain the full traceback for debugging.

♻️ Proposed fix
             except Exception as e:
                 logger.error(f"S3 delete failed for {org.slug}: {e}")
-                raise OrgLogoS3Error("Failed to delete logo from S3")
+                raise OrgLogoS3Error("Failed to delete logo from S3") from e
🤖 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 `@ddpui/core/org_logo/org_logo_service.py` around lines 95 - 100, The except
block in org_logo_service.py catches Exception as e when calling
delete_org_logo(org.logo_s3_key) and re-raises OrgLogoS3Error without preserving
the original exception chain; update the re-raise to use "raise
OrgLogoS3Error('Failed to delete logo from S3') from e" so the original
traceback and context are preserved for debugging while keeping the existing log
call (logger.error(f"S3 delete failed for {org.slug}: {e}")).

41-51: ⚡ Quick win

Preserve exception chain when re-raising.

When converting exceptions to domain-specific errors, use raise ... from e to maintain the full traceback for debugging.

♻️ Proposed fix
         except ValueError as e:
-            raise OrgLogoValidationError(str(e))
+            raise OrgLogoValidationError(str(e)) from e
         except Exception as e:
             logger.error(f"S3 upload failed for {org.slug}: {e}")
-            raise OrgLogoS3Error("Failed to upload logo to S3")
+            raise OrgLogoS3Error("Failed to upload logo to S3") from e
🤖 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 `@ddpui/core/org_logo/org_logo_service.py` around lines 41 - 51, The exception
handlers in the upload block for upload_org_logo currently swallow the original
exceptions; update the except clauses to preserve the exception chain by
re-raising domain errors using "raise ... from e" (i.e., change the
OrgLogoValidationError raise to "raise OrgLogoValidationError(str(e)) from e"
and change the OrgLogoS3Error raise to "raise OrgLogoS3Error(\"Failed to upload
logo to S3\") from e"), keeping the existing logger.error call for S3 failures
so the original traceback is available for debugging.
🤖 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 `@ddpui/api/org_logo_api.py`:
- Around line 64-78: The upload_logo_from_url endpoint (function
upload_logo_from_url) currently calls OrgLogoService.upload_logo_from_url
without any error handling, so any network, validation, or DB exception becomes
a raw 500; wrap the call in a try/except that catches expected errors (e.g.,
network/requests exceptions, ValueError/validation errors, and DB errors like
SQLAlchemyError) and translate them into appropriate HTTP responses using
api_response (success=False with a clear message) or raise HTTPException with
proper status codes (400 for invalid URL/validation, 502 for upstream fetch
errors, 500 for save failures), ensuring OrgLogoResponse.from_model is only
called on success; reference OrgLogoService.upload_logo_from_url and the
api_response/OrgLogoResponse usage when implementing the handling.

In `@ddpui/core/org_logo/org_logo_service.py`:
- Line 1: Run Black to reformat the file ddpui/core/org_logo/org_logo_service.py
and commit the resulting changes; ensure the module-level docstring and the rest
of the file conform to Black's formatting (you can run `black
ddpui/core/org_logo/org_logo_service.py` locally) and include the formatted file
in your commit so CI passes.

In `@ddpui/core/reports/pdf_export_service.py`:
- Around line 114-120: svg detection currently relies on ".svg" in the URL
(inside the page.evaluate that builds svg_items) which misses
extensionless/querystring CDN URLs; instead, collect all image srcs in svg_items
(remove the .svg url filter in page.evaluate) and then, in the server-side code
after page.evaluate, fetch each img.src and determine if it's an SVG by checking
the HTTP Content-Type header for "image/svg+xml" and/or if the response body
starts with "<svg". Use that result to decide which images to send to the
rasterization pass so extensionless/CDN/querystring-served SVGs are correctly
rasterized; update references to svg_items and the rasterization logic
accordingly.
- Around line 123-131: The loop that fetches SVGs (iterate svg_items and
variable svg_url) currently calls http_requests.get(svg_url) unchecked; restrict
this by validating the URL before fetching: parse svg_url (e.g., via
urllib.parse) and allow only trusted schemes/hosts from a configured allowlist
(your S3/CDN/frontend origins) and reject or skip any URLs that resolve to
non-HTTP(S), missing netloc, or to private/RFC1918 IPs; if a URL is external,
avoid calling http_requests.get and instead require that external logos be
re-hosted/ingested earlier in the pipeline. Apply this check right before the
http_requests.get call (where svg_data_uri is created) so only approved origins
are fetched and converted.
- Around line 148-154: The current page.evaluate call in pdf_export_service.py
replaces every <img> with the same src (using svg_url), which breaks cases where
the SVG is reused at different sizes; instead, pass a unique element index or
selector from svg_items into the evaluate call and only replace that specific
node. Update the code that builds svg_items to capture an index or a unique
selector per item, include that index/selector in the args (alongside
svg_url/png_data_uri), and change the page.evaluate callback to target
document.querySelectorAll('img')[index] or document.querySelector(selector) so
only the intended element is swapped; keep references to svg_items, svg_url,
png_data_uri and the page.evaluate invocation to locate and change the logic.

In `@ddpui/schemas/org_schema.py`:
- Line 52: The OrgLogoResponse schema currently declares logo_url as a required
str but the Org model allows null; update the schema in
ddpui/schemas/org_schema.py to accept None by changing logo_url to an optional
type (e.g., Optional[str] with a default of None) so from_model() can pass
org.logo_url when it's null without Pydantic validation errors; locate the
OrgLogoResponse (and any related from_model/static method that maps Org ->
OrgLogoResponse) and make the logo_url type nullable to match the Django Org
model.

---

Nitpick comments:
In `@ddpui/core/org_logo/org_logo_service.py`:
- Around line 95-100: The except block in org_logo_service.py catches Exception
as e when calling delete_org_logo(org.logo_s3_key) and re-raises OrgLogoS3Error
without preserving the original exception chain; update the re-raise to use
"raise OrgLogoS3Error('Failed to delete logo from S3') from e" so the original
traceback and context are preserved for debugging while keeping the existing log
call (logger.error(f"S3 delete failed for {org.slug}: {e}")).
- Around line 41-51: The exception handlers in the upload block for
upload_org_logo currently swallow the original exceptions; update the except
clauses to preserve the exception chain by re-raising domain errors using "raise
... from e" (i.e., change the OrgLogoValidationError raise to "raise
OrgLogoValidationError(str(e)) from e" and change the OrgLogoS3Error raise to
"raise OrgLogoS3Error(\"Failed to upload logo to S3\") from e"), keeping the
existing logger.error call for S3 failures so the original traceback is
available for debugging.

In `@ddpui/utils/s3_utils.py`:
- Line 61: The ValueError message uses an unnecessary f-string prefix; replace
raise ValueError(f"File size exceeds the 5MB limit") with a plain string (remove
the leading f) so it becomes raise ValueError("File size exceeds the 5MB limit")
in the file size check (the raise ValueError(...) statement in
ddpui/utils/s3_utils.py).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6c6fdb2-8c12-4434-9826-97ae0ec5a0a4

📥 Commits

Reviewing files that changed from the base of the PR and between c8e85ad and d12537a.

📒 Files selected for processing (12)
  • .env.template
  • ddpui/api/org_logo_api.py
  • ddpui/api/public_api.py
  • ddpui/core/org_logo/__init__.py
  • ddpui/core/org_logo/exceptions.py
  • ddpui/core/org_logo/org_logo_service.py
  • ddpui/core/reports/pdf_export_service.py
  • ddpui/migrations/0164_org_logo.py
  • ddpui/models/org.py
  • ddpui/routes.py
  • ddpui/schemas/org_schema.py
  • ddpui/utils/s3_utils.py

Comment thread ddpui/api/org_logo_api.py Outdated
Comment thread ddpui/core/org_logo/org_logo_service.py
Comment thread ddpui/core/reports/pdf_export_service.py Outdated
Comment thread ddpui/core/reports/pdf_export_service.py Outdated
Comment thread ddpui/core/reports/pdf_export_service.py Outdated
Comment thread ddpui/schemas/org_schema.py Outdated
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.95%. Comparing base (1cff2fb) to head (ab551e4).

Files with missing lines Patch % Lines
ddpui/core/org_logo/org_logo_service.py 66.29% 30 Missing ⚠️
ddpui/core/reports/pdf_export_service.py 11.11% 24 Missing ⚠️
ddpui/api/org_logo_api.py 77.61% 15 Missing ⚠️
ddpui/utils/s3_utils.py 75.00% 9 Missing ⚠️
ddpui/core/org_logo/exceptions.py 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1406      +/-   ##
==========================================
+ Coverage   59.86%   59.95%   +0.09%     
==========================================
  Files         138      142       +4     
  Lines       16560    16797     +237     
==========================================
+ Hits         9913    10071     +158     
- Misses       6647     6726      +79     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
ddpui/tests/core/org_logo/test_org_logo_service.py (1)

1-185: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Black and commit the reformatted file.

CI indicates this file is reformatted by Black during checks.

🤖 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 `@ddpui/tests/core/org_logo/test_org_logo_service.py` around lines 1 - 185, The
test file ddpui/tests/core/org_logo/test_org_logo_service.py is not formatted to
Black; run the project's Black formatter and commit the changes. Specifically,
run the repository's formatting command (e.g., black . or the configured
pre-commit/format script) to reformat this file, verify tests referencing
OrgLogoService and its test functions still pass, stage the updated
test_org_logo_service.py, and create a commit containing the Black-applied
changes.

Source: Pipeline failures

ddpui/tests/api_tests/test_org_logo_api.py (1)

1-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Run Black and commit the reformatted file.

CI reports this file is not Black-formatted and gets rewritten by pre-commit, which blocks checks.

🤖 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 `@ddpui/tests/api_tests/test_org_logo_api.py` around lines 1 - 145, This file
fails Black formatting; run the Black formatter and commit the updated file so
pre-commit stops rewriting it — e.g., format
ddpui.tests.api_tests.test_org_logo_api (the file containing
test_get_org_logo_not_found_raises_404,
test_upload_logo_file_validation_error_raises_400,
test_upload_logo_file_s3_error_raises_502,
test_upload_logo_from_url_validation_error_raises_400,
test_delete_logo_not_found_raises_404, test_delete_logo_s3_error_raises_502 and
imports like get_org_logo/upload_logo_file/upload_logo_from_url/delete_logo)
with black (or black .) and commit the reformatted file.

Source: Pipeline failures

🧹 Nitpick comments (2)
ddpui/tests/api_tests/test_org_logo_api.py (1)

117-127: ⚡ Quick win

Add a test for the upload-from-URL fallback 500 path.

upload_logo_from_url in ddpui/api/org_logo_api.py also maps unexpected exceptions to HttpError(500), but this contract isn’t covered here. Add one test that patches OrgLogoService.upload_logo_from_url to raise a generic Exception and assert status 500.

🤖 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 `@ddpui/tests/api_tests/test_org_logo_api.py` around lines 117 - 127, Add a new
test in ddpui/tests/api_tests/test_org_logo_api.py that patches
OrgLogoService.upload_logo_from_url to raise a generic Exception and then calls
the API-level upload_logo_from_url (using mock_request(orguser) and
OrgLogoUrlPayload) asserting that the raised HttpError has status_code == 500;
reference the existing test_upload_logo_from_url_validation_error_raises_400 for
structure and reuse mock_request, orguser fixture, HttpError, and
OrgLogoUrlPayload names.
ddpui/tests/core/org_logo/test_org_logo_service.py (1)

178-185: ⚡ Quick win

Strengthen the S3-delete-failure test with state assertions.

After asserting OrgLogoS3Error, also refresh and assert logo_url/logo_s3_key/logo_filename are unchanged. That protects the contract that failed S3 deletion must not clear DB fields.

🤖 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 `@ddpui/tests/core/org_logo/test_org_logo_service.py` around lines 178 - 185,
Update the test_delete_logo_s3_error_raises test to, after the
pytest.raises(OrgLogoS3Error) block, refresh the org instance from the DB and
assert that org_with_logo.logo_url, org_with_logo.logo_s3_key, and
org_with_logo.logo_filename remain unchanged; specifically call
org_with_logo.refresh_from_db() and compare those three attributes to their
original values captured before calling OrgLogoService.delete_logo to guarantee
a failed S3 delete does not clear DB fields.
🤖 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.

Outside diff comments:
In `@ddpui/tests/api_tests/test_org_logo_api.py`:
- Around line 1-145: This file fails Black formatting; run the Black formatter
and commit the updated file so pre-commit stops rewriting it — e.g., format
ddpui.tests.api_tests.test_org_logo_api (the file containing
test_get_org_logo_not_found_raises_404,
test_upload_logo_file_validation_error_raises_400,
test_upload_logo_file_s3_error_raises_502,
test_upload_logo_from_url_validation_error_raises_400,
test_delete_logo_not_found_raises_404, test_delete_logo_s3_error_raises_502 and
imports like get_org_logo/upload_logo_file/upload_logo_from_url/delete_logo)
with black (or black .) and commit the reformatted file.

In `@ddpui/tests/core/org_logo/test_org_logo_service.py`:
- Around line 1-185: The test file
ddpui/tests/core/org_logo/test_org_logo_service.py is not formatted to Black;
run the project's Black formatter and commit the changes. Specifically, run the
repository's formatting command (e.g., black . or the configured
pre-commit/format script) to reformat this file, verify tests referencing
OrgLogoService and its test functions still pass, stage the updated
test_org_logo_service.py, and create a commit containing the Black-applied
changes.

---

Nitpick comments:
In `@ddpui/tests/api_tests/test_org_logo_api.py`:
- Around line 117-127: Add a new test in
ddpui/tests/api_tests/test_org_logo_api.py that patches
OrgLogoService.upload_logo_from_url to raise a generic Exception and then calls
the API-level upload_logo_from_url (using mock_request(orguser) and
OrgLogoUrlPayload) asserting that the raised HttpError has status_code == 500;
reference the existing test_upload_logo_from_url_validation_error_raises_400 for
structure and reuse mock_request, orguser fixture, HttpError, and
OrgLogoUrlPayload names.

In `@ddpui/tests/core/org_logo/test_org_logo_service.py`:
- Around line 178-185: Update the test_delete_logo_s3_error_raises test to,
after the pytest.raises(OrgLogoS3Error) block, refresh the org instance from the
DB and assert that org_with_logo.logo_url, org_with_logo.logo_s3_key, and
org_with_logo.logo_filename remain unchanged; specifically call
org_with_logo.refresh_from_db() and compare those three attributes to their
original values captured before calling OrgLogoService.delete_logo to guarantee
a failed S3 delete does not clear DB fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29bacfd5-079d-4fc5-a181-ab321ba8bdf8

📥 Commits

Reviewing files that changed from the base of the PR and between c4f4c0d and 8bd27b1.

📒 Files selected for processing (5)
  • ddpui/core/reports/pdf_export_service.py
  • ddpui/tests/api_tests/test_org_logo_api.py
  • ddpui/tests/core/org_logo/__init__.py
  • ddpui/tests/core/org_logo/test_org_logo_service.py
  • ddpui/tests/utils/test_s3_utils.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • ddpui/core/reports/pdf_export_service.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ddpui/core/org_logo/org_logo_service.py (1)

72-94: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Critical SSRF vulnerability: validate and restrict logo URLs before saving.

upload_logo_from_url accepts any image_url string and writes it directly to org.logo_url without validation. An attacker can supply internal URLs (http://localhost, http://169.254.169.254/latest/meta-data/, private IP ranges) and later trigger server-side requests to those URLs via the new GET /proxy/ endpoint (which calls get_logo_bytes), leaking internal metadata, services, or cloud credentials.

🛡️ Proposed fix: validate URL scheme and destination

Add validation before line 89 to reject non-HTTP(S) schemes and private/internal destinations:

 `@staticmethod`
 def upload_logo_from_url(image_url: str, org: Org) -> Org:
     """Store an external image URL directly in the DB — no S3 upload.
 
     Args:
         image_url: Public URL of the image to use as logo
         org: The organization
 
     Returns:
         Updated Org instance
     """
+    # Validate URL to prevent SSRF
+    from urllib.parse import urlparse
+    import ipaddress
+    
+    parsed = urlparse(image_url)
+    if parsed.scheme not in ("http", "https"):
+        raise OrgLogoValidationError("Only HTTP and HTTPS URLs are allowed")
+    
+    # Block private/internal IPs
+    try:
+        import socket
+        host = parsed.hostname
+        if not host:
+            raise OrgLogoValidationError("Invalid URL: missing hostname")
+        
+        # Resolve hostname to IP
+        ip = socket.gethostbyname(host)
+        ip_obj = ipaddress.ip_address(ip)
+        
+        if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local:
+            raise OrgLogoValidationError("URLs pointing to private or internal networks are not allowed")
+    except socket.gaierror:
+        raise OrgLogoValidationError("Unable to resolve hostname")
+    
     # Delete old S3 file if the previous logo was uploaded (not a URL)
     if org.logo_s3_key:
🤖 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 `@ddpui/core/org_logo/org_logo_service.py` around lines 72 - 94, The
upload_logo_from_url function currently saves any image_url directly to
org.logo_url; validate and sanitize the URL before saving by (1) parsing
image_url (e.g., via urllib.parse) and rejecting non-http/https schemes and
suspicious schemes like file:, data:, ftp:, (2) resolve and validate the
hostname to an IP (using DNS resolution + ipaddress) and reject private,
loopback, link-local, multicast, or known cloud metadata IPs/ranges (e.g.,
169.254.169.254/32) and localhost names, and (3) optionally restrict to a safe
allowlist of hostnames or require a public IP. If validation fails, raise/return
a validation error and do not modify org; only proceed to clear
org.logo_s3_key/logo_filename, set org.logo_url and call
org.save(update_fields=[...]) when the URL passes validation. Ensure the checks
are applied inside upload_logo_from_url before changing org and before logging.
🧹 Nitpick comments (2)
ddpui/api/org_logo_api.py (2)

104-115: ⚡ Quick win

Add cache headers to reduce repeated logo fetches.

The /proxy/ endpoint fetches the logo from external storage on every request. If logos are immutable or change infrequently, adding Cache-Control headers would reduce backend load and improve client performance.

⚡ Proposed enhancement
     try:
         image_bytes, content_type = OrgLogoService.get_logo_bytes(orguser.org)
-        return HttpResponse(image_bytes, content_type=content_type)
+        response = HttpResponse(image_bytes, content_type=content_type)
+        # Cache for 1 hour (adjust based on logo update frequency)
+        response["Cache-Control"] = "public, max-age=3600"
+        return response
     except OrgLogoNotFoundError as e:

Alternatively, use ETag or Last-Modified headers if you track logo update timestamps.

🤖 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 `@ddpui/api/org_logo_api.py` around lines 104 - 115, The proxy_org_logo_image
endpoint currently returns raw bytes but lacks caching headers; update the
HttpResponse returned in proxy_org_logo_image (inside the try block after
OrgLogoService.get_logo_bytes) to include appropriate cache headers (e.g., set
"Cache-Control: public, max-age=86400, immutable" or another suitable max-age)
and optionally add ETag or Last-Modified headers if you can derive a
version/timestamp from OrgLogoService or org (use OrgLogoService.get_logo_bytes
return metadata or another method to compute an ETag); ensure headers are set on
the HttpResponse before returning and keep existing error handling
(OrgLogoNotFoundError, OrgLogoS3Error) intact.

82-83: 💤 Low value

Remove unreachable exception handler.

The service method OrgLogoService.upload_logo_from_url (in ddpui/core/org_logo/org_logo_service.py lines 72-94) performs only DB writes and S3 deletion; it never raises OrgLogoValidationError. This catch block will never execute.

♻️ Proposed cleanup
         return api_response(
             success=True,
             data=OrgLogoResponse.from_model(org),
             message="Logo URL saved successfully",
         )
-    except OrgLogoValidationError as e:
-        raise HttpError(400, str(e)) from e
     except Exception as e:

Note: If you accept the SSRF fix proposed in ddpui/core/org_logo/org_logo_service.py (lines 72-94), the service will raise OrgLogoValidationError for invalid URLs, so this handler should be kept.

🤖 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 `@ddpui/api/org_logo_api.py` around lines 82 - 83, The except block catching
OrgLogoValidationError in the upload endpoint should be removed because
OrgLogoService.upload_logo_from_url does not raise OrgLogoValidationError;
delete the except OrgLogoValidationError handler and its raise HttpError(400,
...) line from the upload handler (the code referencing OrgLogoValidationError
in org_logo_api.py) so only relevant exceptions are handled or let higher-level
error middleware handle unexpected errors; if you later apply the SSRF
validation change inside OrgLogoService.upload_logo_from_url that begins raising
OrgLogoValidationError, reintroduce this specific catch to convert it to
HttpError(400).
🤖 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 `@ddpui/core/org_logo/org_logo_service.py`:
- Around line 31-35: Raise a new, non-S3-specific exception when HTTP fetching
fails: add OrgLogoFetchError(OrgLogoError) (message + code
"ORG_LOGO_FETCH_ERROR") in exceptions.py, then in the code that performs
http_requests.get(org.logo_url, ...) replace the raised OrgLogoS3Error calls
with OrgLogoFetchError for both the RequestException branch and the resp.ok
check so failures from external HTTP(S) endpoints are correctly classified;
finally update the API layer error handling (where OrgLogoS3Error is currently
caught) to also handle or map OrgLogoFetchError so callers receive the
appropriate error code.
- Line 31: Validate org.logo_url’s URL scheme before calling http_requests.get:
parse org.logo_url (e.g., using urllib.parse.urlparse or
requests.utils.urlparse) and ensure scheme is exactly "http" or "https"; if not,
log or handle the invalid URL and skip the fetch instead of calling
http_requests.get(org.logo_url, timeout=10). Update the code around the resp =
http_requests.get(...) call to perform this check and only call
http_requests.get when the scheme is allowed, returning an appropriate
error/None when rejected.
- Around line 25-37: The get_logo_bytes function currently reads resp.content
with no size limit; change the fetch to request a streamed response (use
http_requests.get(..., stream=True)) and read the body incrementally (e.g., via
resp.iter_content or chunked reads) up to a fixed maximum byte cap (e.g., a few
MB), closing the response and raising OrgLogoS3Error if the body exceeds that
cap or on read errors; keep using org.logo_url and the existing timeout and
preserve returning a (bytes, content_type) tuple when within limit.

---

Outside diff comments:
In `@ddpui/core/org_logo/org_logo_service.py`:
- Around line 72-94: The upload_logo_from_url function currently saves any
image_url directly to org.logo_url; validate and sanitize the URL before saving
by (1) parsing image_url (e.g., via urllib.parse) and rejecting non-http/https
schemes and suspicious schemes like file:, data:, ftp:, (2) resolve and validate
the hostname to an IP (using DNS resolution + ipaddress) and reject private,
loopback, link-local, multicast, or known cloud metadata IPs/ranges (e.g.,
169.254.169.254/32) and localhost names, and (3) optionally restrict to a safe
allowlist of hostnames or require a public IP. If validation fails, raise/return
a validation error and do not modify org; only proceed to clear
org.logo_s3_key/logo_filename, set org.logo_url and call
org.save(update_fields=[...]) when the URL passes validation. Ensure the checks
are applied inside upload_logo_from_url before changing org and before logging.

---

Nitpick comments:
In `@ddpui/api/org_logo_api.py`:
- Around line 104-115: The proxy_org_logo_image endpoint currently returns raw
bytes but lacks caching headers; update the HttpResponse returned in
proxy_org_logo_image (inside the try block after OrgLogoService.get_logo_bytes)
to include appropriate cache headers (e.g., set "Cache-Control: public,
max-age=86400, immutable" or another suitable max-age) and optionally add ETag
or Last-Modified headers if you can derive a version/timestamp from
OrgLogoService or org (use OrgLogoService.get_logo_bytes return metadata or
another method to compute an ETag); ensure headers are set on the HttpResponse
before returning and keep existing error handling (OrgLogoNotFoundError,
OrgLogoS3Error) intact.
- Around line 82-83: The except block catching OrgLogoValidationError in the
upload endpoint should be removed because OrgLogoService.upload_logo_from_url
does not raise OrgLogoValidationError; delete the except OrgLogoValidationError
handler and its raise HttpError(400, ...) line from the upload handler (the code
referencing OrgLogoValidationError in org_logo_api.py) so only relevant
exceptions are handled or let higher-level error middleware handle unexpected
errors; if you later apply the SSRF validation change inside
OrgLogoService.upload_logo_from_url that begins raising OrgLogoValidationError,
reintroduce this specific catch to convert it to HttpError(400).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 325d075f-bd31-4b6b-a19f-36d395340d5d

📥 Commits

Reviewing files that changed from the base of the PR and between 8bd27b1 and 6ff4ce9.

📒 Files selected for processing (4)
  • ddpui/api/org_logo_api.py
  • ddpui/core/org_logo/org_logo_service.py
  • ddpui/tests/api_tests/test_org_logo_api.py
  • ddpui/tests/core/org_logo/test_org_logo_service.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • ddpui/tests/api_tests/test_org_logo_api.py
  • ddpui/tests/core/org_logo/test_org_logo_service.py

Comment thread ddpui/core/org_logo/org_logo_service.py Outdated
Comment thread ddpui/core/org_logo/org_logo_service.py Outdated
if not org.logo_url:
raise OrgLogoNotFoundError()
try:
resp = http_requests.get(org.logo_url, timeout=10)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restrict URL schemes to HTTP(S) only.

requests.get(org.logo_url, ...) will follow any scheme supported by the requests library (including file://, ftp://, etc.). If org.logo_url contains a non-HTTP(S) scheme, this could expose local files or trigger unintended protocol handlers.

🔒 Proposed fix: validate scheme before fetch
+    from urllib.parse import urlparse
+    
     if not org.logo_url:
         raise OrgLogoNotFoundError()
+    
+    parsed = urlparse(org.logo_url)
+    if parsed.scheme not in ("http", "https"):
+        raise OrgLogoValidationError(f"Unsupported URL scheme: {parsed.scheme}")
+    
     try:
         resp = http_requests.get(org.logo_url, timeout=10)
🤖 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 `@ddpui/core/org_logo/org_logo_service.py` at line 31, Validate org.logo_url’s
URL scheme before calling http_requests.get: parse org.logo_url (e.g., using
urllib.parse.urlparse or requests.utils.urlparse) and ensure scheme is exactly
"http" or "https"; if not, log or handle the invalid URL and skip the fetch
instead of calling http_requests.get(org.logo_url, timeout=10). Update the code
around the resp = http_requests.get(...) call to perform this check and only
call http_requests.get when the scheme is allowed, returning an appropriate
error/None when rejected.

Comment thread ddpui/core/org_logo/org_logo_service.py Outdated
@himanshudube97 himanshudube97 self-requested a review June 11, 2026 06:19
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