Skip to content

Conversation

@jbpenrath
Copy link
Contributor

@jbpenrath jbpenrath commented Nov 4, 2025

Purpose

Install django-lasuite and our custom authentication backend to inherit
from the LaSuiteOIDCAuthenticationBackend

Setup Drive resource server query to retrieve and save files from a configured
Drive instance through OIDC resource server.

CleanShot.2025-11-04.at.09.58.25.mp4

Summary by CodeRabbit

  • New Features

    • Save message attachments to your personal Drive workspace and preview Drive files directly from messages (upload & preview UI).
  • Bug Fixes

    • Improved attachment display and sizing within message bodies.
  • Documentation

    • Updated configuration docs with new Drive and OIDC environment settings.
  • Chores

    • Authentication flow and session token handling updated; OIDC-related settings renamed and new token storage options added.

@coderabbitai
Copy link

coderabbitai bot commented Nov 4, 2025

Walkthrough

Migrates OIDC from mozilla-django-oidc to django-lasuite, adds an interop Drive API (GET/POST) with backend handling and OpenAPI schemas, introduces frontend Drive upload/preview components, renames and adds OIDC/session config, and updates tests and locales accordingly.

Changes

Cohort / File(s) Summary
OIDC auth migration
src/backend/core/authentication/backends.py, src/backend/core/authentication/urls.py, src/backend/core/authentication/views.py, src/backend/core/tests/authentication/test_backends.py, src/backend/core/tests/authentication/test_urls.py, src/backend/core/tests/authentication/test_views.py
Replace mozilla-django-oidc with django-lasuite; new backend API (get_or_create_user, post_get_or_create_user, helpers); remove custom logout views/routes; update/remove related tests.
Drive API (backend)
src/backend/core/api/viewsets/drive.py, src/backend/core/api/utils.py, src/backend/core/urls.py
Add DriveAPIView (GET list, POST upload) that proxies to external Drive service with OIDC token refresh; add get_attachment_from_blob_id utility; register route under /api/v{version}/interop/drive/.
OpenAPI / serializers
src/backend/core/api/openapi.json, src/backend/core/api/serializers.py
Add DriveUploadAttachmentRequest, PaginatedDriveItemResponse, PartialDriveItem schemas; remove id from Attachment representation; add PartialDriveItemSerializer.
Config & settings
src/backend/core/api/viewsets/config.py, src/backend/messages/settings.py, docs/env.md, src/backend/core/tests/api/test_config.py
Add DRIVE.file_url, include file_url in runtime response; rename USER_OIDC_* env vars to OIDC_USERINFO_*; add OIDC_STORE_ACCESS_TOKEN, OIDC_STORE_REFRESH_TOKEN, OIDC_STORE_REFRESH_TOKEN_KEY; update docs and tests.
Backend tests (Drive)
src/backend/core/tests/api/test_drive.py
New comprehensive tests for Drive GET/POST flows, auth/token refresh handling, blob resolution, upload-to-presigned-url flow, and error cases.
Dependencies
src/backend/pyproject.toml
Remove mozilla-django-oidc, add django-lasuite[all]==0.0.14.
Frontend — Drive UI
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx, .../drive-preview-link.tsx, .../attachment-item.tsx, .../_index.scss
Add DriveUploadButton (state machine, caching, upload mutation) and DrivePreviewLink; update AttachmentItem to conditionally render upload/preview and accept canDownload; add tooltip/container styles.
Frontend — attachment types & uploader
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx, .../attachment-uploader.tsx
Extend DriveFile type to include id; tighten id-matching logic in attachment removal; pass canDownload prop to AttachmentItem.
Frontend — locales & minor tweaks
src/frontend/public/locales/common/en-US.json, src/frontend/public/locales/common/fr-FR.json, src/frontend/src/features/forms/components/message-composer/index.tsx, src/frontend/src/features/layouts/components/thread-view/components/thread-message/message-body.tsx
Add Drive-related translation keys (EN/FR); remove a comment in composer; constrain iframe images to max-width:100vw.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Drive
    participant S3

    rect rgb(210,230,255)
    Note over User,Drive: Drive listing flow
    User->>Frontend: open Drive picker / request list
    Frontend->>Backend: GET /api/v1.0/interop/drive/?title=...
    Backend->>Backend: ensure OIDC access token (refresh if needed)
    Backend->>Drive: GET /workspaces (Authorization: Bearer ...)
    Drive-->>Backend: list workspaces
    Backend->>Drive: GET /workspaces/{main}/children?title=...
    Drive-->>Backend: paginated file list
    Backend-->>Frontend: PaginatedDriveItemResponse
    end
Loading
sequenceDiagram
    participant User
    participant Frontend
    participant Backend
    participant Drive
    participant S3

    rect rgb(220,255,220)
    Note over User,S3: Drive upload flow
    User->>Frontend: click upload (blob_id)
    Frontend->>Backend: POST /api/v1.0/interop/drive/ { blob_id }
    Backend->>Backend: resolve blob_id -> Attachment (access checks)
    Backend->>Drive: POST /files (create file entry)
    Drive-->>Backend: { id, presigned_upload_url, upload_end_url }
    Backend->>S3: PUT to presigned_upload_url (attachment bytes)
    S3-->>Backend: 200 OK
    Backend->>Drive: POST upload-ended callback
    Drive-->>Backend: file metadata (PartialDriveItem)
    Backend-->>Frontend: PartialDriveItem
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–30 minutes

  • Review focus:
    • OIDC migration: backend hooks, token storage/encryption, removed logout paths.
    • Drive API: token refresh usage, blob-id decoding, access checks, external request error handling.
    • Tests: ensure mocks align with external interactions.
    • Frontend: DriveUploadButton state machine, caching, and cache updates after upload.
    • Schema change: Attachment.id removal impact.

Possibly related PRs

Suggested reviewers

  • sylvinus
  • sdemagny

Poem

🐰 I found a token, neat and spry,

Lasuite helped it fly on high.
Files hop in with one small click,
Previews blink and uploads stick.
A carrot-toast to code — hop quick!

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being implemented: Drive interoperability for saving attachments to a Drive workspace.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/drive-interop-save-attachment

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.2)
src/backend/core/api/serializers.py
src/backend/core/api/utils.py
src/backend/core/api/viewsets/config.py
  • 9 others

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.

@sylvinus sylvinus self-requested a review November 4, 2025 10:18
@jbpenrath jbpenrath force-pushed the feat/drive-interop-save-attachment branch from 61005ab to fccc491 Compare November 4, 2025 17:47
@jbpenrath jbpenrath marked this pull request as ready for review November 4, 2025 17:47
@jbpenrath jbpenrath self-assigned this Nov 4, 2025
Insatll django-lasuite and our custom authentication backend to inherit
from the LaSuiteOIDCAuthenticationBackend
Setup Drive resource server query to retrieve and save files from a configured
Drive instance through OIDC resource server.

suitenumerique/drive#379
@jbpenrath jbpenrath force-pushed the feat/drive-interop-save-attachment branch from fccc491 to bf29028 Compare November 4, 2025 17:53
Copy link

@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: 8

Caution

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

⚠️ Outside diff range comments (2)
src/backend/core/api/serializers.py (1)

374-399: Verify that frontend code will break due to attachment id field removal.

The search confirms this is indeed a breaking change:

  1. Frontend code uses attachment.id in two locations:

    • src/frontend/src/features/utils/mail-helper.tsx:163 - uses a.id in data-id attribute
    • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx:87 - uses attachment.id as fileId parameter to DrivePreviewLink
  2. Current serializer does not expose id:

    • The AttachmentSerializer.Meta.fields list (lines 374-399) does not include "id"
    • Only exposes: blobId, name, size, type, sha256, created_at, cid
  3. Required actions before merge:

    • Update frontend code in both locations to use an alternative identifier (likely blobId instead of id)
    • Ensure API consumers are aligned with this change
    • Consider deprecation approach if this is a public API
src/backend/core/authentication/backends.py (1)

34-86: Fix two critical method call errors in get_or_create_user (lines 51, 78)

  1. Line 51: get_userinfo(access_token, id_token, payload) passes wrong parameter. Parent method signature expects get_userinfo(self, access_token, id_token=None, userinfo_endpoint=None). Replace payload with userinfo_endpoint or remove the third argument if not needed.

  2. Line 78: self.update_user_if_needed(user, claims) calls undefined method. Parent class provides update_user(self, user, claims), not update_user_if_needed. Either rename to update_user or define the custom method in this class.

🧹 Nitpick comments (13)
src/frontend/public/locales/common/en-US.json (1)

54-55: Minor English phrasing suggestions for improved clarity.

The translations are complete and properly formatted. The French equivalents are high-quality. However, two English strings could use slightly more natural phrasing:

  • Line 251: "Save into your Drive's workspace" → Consider "Save to your Drive's workspace" (more idiomatic)
  • Lines 54-55: The passive constructions are acceptable but could be tightened if consistency with other UI messages suggests an active voice style

These are minor stylistic suggestions; the current phrasing is acceptable and matches the PR's localization additions.

Also applies to: 226-226, 251-251

src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss (1)

42-42: Remove commented-out code.

If the gap is no longer needed for the attachment actions layout, remove the commented line rather than leaving it in the codebase.

Apply this diff if the gap is definitively not needed:

 .attachment-item-actions {
     display: flex;
     flex-direction: row;
-    // gap: var(--c--theme--spacings--2xs);
     flex-wrap: nowrap;
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx (1)

86-92: Consider extracting timeout constants.

The hardcoded timeout values (1500ms for success, 5000ms for error) could be defined as named constants to improve maintainability and make the durations more discoverable.

For example, at the top of the file:

const SUCCESS_TIMEOUT_MS = 1500;
const ERROR_TIMEOUT_MS = 5000;

Then use:

-            }, state === 'success' ? 1500 : 5000);
+            }, state === 'success' ? SUCCESS_TIMEOUT_MS : ERROR_TIMEOUT_MS);
docs/env.md (1)

157-162: Good documentation additions, but consider clarifying security requirements.

The new OIDC configuration variables are well-documented and follow the existing format. However, consider enhancing the documentation:

  1. OIDC_STORE_REFRESH_TOKEN_KEY - While the note about Fernet key is helpful, consider adding:

    • A note that this becomes required when OIDC_STORE_REFRESH_TOKEN is True
    • A reference to how to generate a valid Fernet key
    • A security warning about key rotation and storage
  2. Consider adding an example or reference link for generating a Fernet key, such as:

    from cryptography.fernet import Fernet
    key = Fernet.generate_key()
src/backend/core/api/openapi.json (5)

186-196: Add URI format for DRIVE URLs.

file_url is a URL; declare "format": "uri" for validation. Consider adding the same to sdk_url and api_url for consistency.

-                                                "file_url": {
-                                                    "type": "string",
-                                                    "readOnly": true
-                                                }
+                                                "file_url": {
+                                                    "type": "string",
+                                                    "format": "uri",
+                                                    "readOnly": true
+                                                }

1660-1735: Interop Drive: fix tag, status code, and basic ergonomics.

  • Tags: GET uses interop/drive, POST uses interop. Use one tag for both.
  • Status: POST should return 201 for create.
  • Pagination: Response is paginated but no page (or equivalent) query param.
  • Errors: Add 400/403/404 like other endpoints.
  • Param naming: consider q (optional) to align with search endpoints elsewhere; if staying with title, consider not required.
-            "get": {
+            "get": {
               "operationId": "interop_drive_retrieve",
               "description": "Search for files created by the current user.",
               "parameters": [
+                {
+                  "name": "page",
+                  "in": "query",
+                  "required": false,
+                  "schema": { "type": "integer" },
+                  "description": "A page number within the paginated result set."
+                },
                 {
                   "in": "query",
                   "name": "title",
                   "schema": {
                     "type": "string"
                   },
                   "description": "Search files by title.",
-                  "required": true
+                  "required": true
                 }
               ],
-              "tags": [ "interop/drive" ],
+              "tags": [ "interop/drive" ],
               "security": [ { "cookieAuth": [] } ],
               "responses": {
                 "200": { ... },
+                "400": { "description": "Bad request" },
+                "403": { "description": "Permission denied" },
+                "404": { "description": "Not found" }
               }
             },
             "post": {
               "operationId": "interop_drive_create",
               "description": "Create a new file in the main workspace.",
-              "tags": [ "interop" ],
+              "tags": [ "interop/drive" ],
               "requestBody": { ... },
               "security": [ { "cookieAuth": [] } ],
               "responses": {
-                "200": {
+                "201": {
                   "content": {
                     "application/json": {
                       "schema": { "$ref": "#/components/schemas/PartialDriveItem" }
                     }
                   },
                   "description": "File created successfully"
-                }
+                },
+                "400": { "description": "Bad request" },
+                "403": { "description": "Permission denied" },
+                "404": { "description": "Not found" }
               }
             }

5302-5314: Unify JSON field casing and wording (blob_id → blobId).

API elsewhere uses camelCase (blobId, messageId). Rename for consistency and clarify description (“blob” vs “attachment”).

-            "DriveUploadAttachmentRequest": {
+            "DriveUploadAttachmentRequest": {
               "type": "object",
               "properties": {
-                "blob_id": {
+                "blobId": {
                   "type": "string",
                   "format": "uuid",
-                  "description": "ID of the attachment to upload"
+                  "description": "ID of the blob to upload"
                 }
               },
               "required": [
-                "blob_id"
+                "blobId"
               ]
             },

If this API is already consumed, consider supporting both names temporarily and deprecating blob_id.


6600-6627: Align pagination schema with existing Paginated types.*

  • Don’t require next/previous (they can be absent/null). Other paginated schemas only require count and results.
  • Add "format": "uri" to next/previous.
-            "PaginatedDriveItemResponse": {
+            "PaginatedDriveItemResponse": {
               "type": "object",
               "properties": {
                 "count": { "type": "integer" },
-                "next": { "type": "string", "nullable": true },
-                "previous": { "type": "string", "nullable": true },
+                "next": { "type": "string", "format": "uri", "nullable": true },
+                "previous": { "type": "string", "format": "uri", "nullable": true },
                 "results": {
                   "type": "array",
                   "items": { "$ref": "#/components/schemas/PartialDriveItem" }
                 }
               },
               "required": [
-                "count","next","previous","results"
+                "count","results"
               ]
             },

6814-6838: Verify Drive item ID format; relax if not guaranteed UUID.

External Drive IDs are often opaque strings. If not guaranteed UUID, drop "format": "uuid" to avoid client-side validation failures.

-          "id": { "type": "string", "format": "uuid" },
+          "id": { "type": "string" },
src/backend/core/authentication/backends.py (4)

50-85: Consider adding logging for authentication events

Per coding guidelines, log expected and unexpected actions with appropriate log levels. Consider adding logging for:

  • User creation (line 81-82)
  • Existing user authentication (line 75)
  • Authentication failures (lines 54-55, 59-61)

Example:

         elif self.should_create_user(email):
             user = self.create_user(claims)
             _user_created = True
+            logger.info("Created new user with sub=%s", sub)

As per coding guidelines


50-85: The _user_created flag appears unused in the post-hook

The _user_created flag is passed to post_get_or_create_user but that method doesn't use it (line 87-90). Consider either using it to conditionally perform setup actions, or removing it if unnecessary.


87-90: Unused parameters in post-hook

The claims and _user_created parameters are not used in this method. If they're intended for future use or subclass overrides, consider documenting this in the docstring. Otherwise, they could be removed to reduce confusion.


98-103: Good error handling for duplicate email scenario

The method correctly wraps the DuplicateEmailError as a SuspiciousOperation and properly chains the exception. Consider adding a log statement before raising the exception to track these security-relevant events.

As per coding guidelines

     def get_existing_user(self, sub, email):
         """Get an existing user by sub or email."""
         try:
             return User.objects.get_user_by_sub_or_email(sub, email)
         except DuplicateEmailError as err:
+            logger.warning("Duplicate email detected: %s", err.message)
             raise SuspiciousOperation(err.message) from err
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afcced7 and bf29028.

⛔ Files ignored due to path filters (11)
  • src/backend/poetry.lock is excluded by !**/*.lock
  • src/frontend/src/features/api/gen/index.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/interop-drive/interop-drive.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/interop/interop.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/attachment.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/drive_upload_attachment_request.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/index.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/interop_drive_retrieve_params.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/paginated_drive_item_response.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/partial_drive_item.ts is excluded by !**/gen/**
📒 Files selected for processing (28)
  • docs/env.md (1 hunks)
  • src/backend/core/api/openapi.json (5 hunks)
  • src/backend/core/api/serializers.py (1 hunks)
  • src/backend/core/api/utils.py (2 hunks)
  • src/backend/core/api/viewsets/blob.py (2 hunks)
  • src/backend/core/api/viewsets/config.py (2 hunks)
  • src/backend/core/api/viewsets/drive.py (1 hunks)
  • src/backend/core/authentication/backends.py (3 hunks)
  • src/backend/core/authentication/urls.py (1 hunks)
  • src/backend/core/authentication/views.py (0 hunks)
  • src/backend/core/tests/api/test_config.py (2 hunks)
  • src/backend/core/tests/api/test_drive.py (1 hunks)
  • src/backend/core/tests/authentication/test_backends.py (8 hunks)
  • src/backend/core/tests/authentication/test_urls.py (0 hunks)
  • src/backend/core/tests/authentication/test_views.py (0 hunks)
  • src/backend/core/urls.py (2 hunks)
  • src/backend/messages/settings.py (2 hunks)
  • src/backend/pyproject.toml (1 hunks)
  • src/frontend/public/locales/common/en-US.json (3 hunks)
  • src/frontend/public/locales/common/fr-FR.json (3 hunks)
  • src/frontend/src/features/forms/components/message-composer/index.tsx (0 hunks)
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (2 hunks)
  • src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1 hunks)
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss (3 hunks)
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx (3 hunks)
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-preview-link.tsx (1 hunks)
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx (1 hunks)
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/message-body.tsx (1 hunks)
💤 Files with no reviewable changes (4)
  • src/frontend/src/features/forms/components/message-composer/index.tsx
  • src/backend/core/tests/authentication/test_urls.py
  • src/backend/core/authentication/views.py
  • src/backend/core/tests/authentication/test_views.py
🧰 Additional context used
📓 Path-based instructions (6)
src/backend/**/*.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/*.py: Follow Django/PEP 8 style with a 100-character line limit
Use descriptive, snake_case names for variables and functions
Use Django ORM for database access; avoid raw SQL unless necessary for performance
Use Django’s built-in user model and authentication framework
Prefer try-except blocks to handle exceptions in business logic and views
Log expected and unexpected actions with appropriate log levels
Capture and report exceptions to Sentry; use capture_exception() for custom errors
Do not log sensitive information (tokens, passwords, financial/health data, PII)

Files:

  • src/backend/core/api/viewsets/config.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/authentication/urls.py
  • src/backend/core/tests/authentication/test_backends.py
  • src/backend/core/api/viewsets/drive.py
  • src/backend/core/tests/api/test_drive.py
  • src/backend/core/api/utils.py
  • src/backend/core/urls.py
  • src/backend/core/api/serializers.py
  • src/backend/core/authentication/backends.py
  • src/backend/messages/settings.py
src/backend/**/{tests.py,tests/**/*.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/{tests.py,tests/**/*.py}: Use Django’s testing tools (pytest-django) to ensure code quality and reliability
Unit tests should focus on a single use case, keep assertions minimal, and cover all possible cases

Files:

  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/authentication/test_backends.py
  • src/backend/core/tests/api/test_drive.py
src/backend/**/urls.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Define clear, RESTful URL patterns using Django’s URL dispatcher

Files:

  • src/backend/core/authentication/urls.py
  • src/backend/core/urls.py
src/backend/{templates/**/*.html,**/serializers.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use Django templates for HTML and DRF serializers for JSON responses

Files:

  • src/backend/core/api/serializers.py
src/backend/**/{settings.py,middleware.py}

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

Use middleware judiciously for cross-cutting concerns (authentication, logging, caching)

Files:

  • src/backend/messages/settings.py
src/backend/**/settings.py

📄 CodeRabbit inference engine (.cursor/rules/django-python.mdc)

src/backend/**/settings.py: Leverage Django’s caching framework (e.g., Redis/Memcached) where appropriate
Use Django’s cache framework with a backend like Redis or Memcached to reduce DB load
Optimize static file handling using Django’s staticfiles pipeline (e.g., WhiteNoise)

Files:

  • src/backend/messages/settings.py
🧠 Learnings (2)
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/views.py : Use Django REST Framework viewsets for API endpoints

Applied to files:

  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/urls.py
📚 Learning: 2025-09-02T10:12:12.835Z
Learnt from: CR
Repo: suitenumerique/messages PR: 0
File: .cursor/rules/django-python.mdc:0-0
Timestamp: 2025-09-02T10:12:12.835Z
Learning: Applies to src/backend/**/urls.py : Define clear, RESTful URL patterns using Django’s URL dispatcher

Applied to files:

  • src/backend/core/urls.py
🧬 Code graph analysis (13)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx (4)
src/frontend/src/features/api/gen/models/attachment.ts (1)
  • Attachment (12-27)
src/frontend/src/features/api/gen/interop-drive/interop-drive.ts (1)
  • useInteropDriveRetrieve (195-224)
src/frontend/src/features/api/gen/interop/interop.ts (1)
  • useInteropDriveCreate (99-119)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-preview-link.tsx (1)
  • DrivePreviewLink (15-32)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)
src/frontend/src/features/api/gen/models/attachment.ts (1)
  • Attachment (12-27)
src/backend/core/api/viewsets/config.py (1)
src/backend/core/api/viewsets/drive.py (1)
  • get (61-108)
src/backend/core/api/viewsets/blob.py (1)
src/backend/core/api/utils.py (1)
  • get_attachment_from_blob_id (73-103)
src/backend/core/tests/authentication/test_backends.py (3)
src/backend/core/authentication/backends.py (2)
  • OIDCAuthenticationBackend (27-205)
  • get_or_create_user (34-85)
src/backend/core/factories.py (1)
  • UserFactory (20-31)
src/backend/core/models.py (1)
  • User (127-240)
src/backend/core/api/viewsets/drive.py (3)
src/backend/core/api/serializers.py (6)
  • PartialDriveItemSerializer (1474-1494)
  • update (1098-1148)
  • update (1179-1181)
  • update (1420-1452)
  • update (1470-1471)
  • update (1493-1494)
src/backend/core/api/utils.py (1)
  • get_attachment_from_blob_id (73-103)
src/backend/core/models.py (1)
  • Blob (1425-1520)
src/backend/core/tests/api/test_drive.py (2)
src/backend/core/factories.py (7)
  • UserFactory (20-31)
  • MailboxFactory (60-96)
  • MailboxAccessFactory (99-109)
  • ThreadFactory (125-132)
  • ThreadAccessFactory (135-145)
  • MessageFactory (160-186)
  • raw_mime (173-186)
src/backend/core/api/viewsets/drive.py (2)
  • get (61-108)
  • post (129-211)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-preview-link.tsx (2)
src/frontend/src/features/providers/config.tsx (1)
  • useConfig (48-54)
src/frontend/src/features/forms/components/message-form/drive-icon.tsx (1)
  • DriveIcon (3-15)
src/backend/core/api/utils.py (1)
src/backend/core/models.py (3)
  • ThreadAccess (1101-1123)
  • Blob (1425-1520)
  • get_parsed_data (1262-1271)
src/backend/core/urls.py (1)
src/backend/core/api/viewsets/drive.py (1)
  • DriveAPIView (26-211)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx (2)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx (1)
  • DriveUploadButton (25-138)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-preview-link.tsx (1)
  • DrivePreviewLink (15-32)
src/backend/core/api/serializers.py (1)
src/backend/core/models.py (17)
  • size (1567-1569)
  • Meta (89-90)
  • Meta (199-202)
  • Meta (283-286)
  • Meta (454-466)
  • Meta (501-506)
  • Meta (718-722)
  • Meta (761-764)
  • Meta (944-949)
  • Meta (1116-1120)
  • Meta (1137-1141)
  • Meta (1178-1182)
  • Meta (1253-1257)
  • Meta (1484-1496)
  • Meta (1552-1556)
  • Meta (1592-1596)
  • Meta (1646-1650)
src/backend/core/authentication/backends.py (1)
src/backend/core/models.py (3)
  • User (127-240)
  • get_user_by_sub_or_email (101-124)
  • DuplicateEmailError (51-58)
🔇 Additional comments (38)
src/backend/pyproject.toml (1)

40-40: Verify django-lasuite version stability and scope of [all] extras.

You're pinning django-lasuite[all]==0.0.14, which is an early-stage version (0.x). Combined with the broad [all] feature flag, this introduces several risks:

  1. Stability: Version 0.0.14 is pre-release software and may introduce breaking changes in upcoming patches/minors.
  2. Scope: Installing all extras ([all]) may pull in unused or conflicting dependencies. Only the specific extras needed for Drive interop should be included.
  3. Migration completeness: The AI summary mentions removing mozilla-django-oidc==4.0.1, but it's not visible in this diff. Verify the old OIDC dependency is actually removed from this list and all configuration files.

Please verify the following and refine if needed:

  • Is 0.0.14 the latest stable release of django-lasuite, or are there newer versions available?
  • Which specific extras does the Drive interop integration require? Consider replacing [all] with only the necessary extras.
  • Confirm that mozilla-django-oidc==4.0.1 has been removed from the dependencies list.
src/frontend/public/locales/common/fr-FR.json (1)

54-55: Excellent French translations—idiomatic and consistent.

The French localization is well-executed. All four Drive-related strings:

  • Use natural, idiomatic French phrasing
  • Consistently substitute "Fichiers" for "Drive," which is appropriate for the French audience
  • Are properly aligned with their English counterparts
  • Match the style of existing French messages in the file (e.g., line 25)

The translations strengthen the user experience for French-speaking users.

Also applies to: 226-226, 251-251

src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (2)

11-11: LGTM: DriveFile type expanded appropriately.

The addition of the id field to the DriveFile type aligns with the Drive integration requirements, enabling proper identification for preview links and upload tracking.


31-38: LGTM: Serialization logic is correct.

The serializeToDriveFile function properly maps Drive SDK items to the expected DriveFile structure, including the new id field.

src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-preview-link.tsx (1)

15-31: LGTM: Clean and accessible Drive preview link component.

The component properly constructs the preview URL using the Drive config and provides appropriate ARIA labels for accessibility.

src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx (2)

62-68: LGTM: Type-safe removal logic for mixed attachment types.

The updated logic correctly handles both Attachment (by blobId) and DriveFile (by id) items, preventing cross-type matches.


179-185: LGTM: Explicit canDownload prop improves clarity.

Disabling downloads in the attachment uploader context is appropriate since files are still being composed.

src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx (3)

22-27: LGTM: Effective type guards for attachment discrimination.

The predicates correctly distinguish between Attachment and DriveFile types, enabling type-safe conditional rendering.


72-87: LGTM: Logical conditional rendering for Drive integration.

The rendering correctly differentiates between local Attachment items (showing upload-to-Drive option) and DriveFile items (showing preview link), providing appropriate actions for each type.


38-47: LGTM: Error handling and Drive visual indicators preserved.

The variant-based icon rendering maintains error states while adding appropriate Drive branding for DriveFile items.

src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss (1)

141-180: LGTM: Comprehensive Drive upload tooltip styling.

The tooltip styles provide smooth animations and appropriate visual feedback for both success and error states.

src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx (4)

31-40: LGTM: Well-configured Drive file query.

The query configuration appropriately balances performance (10-minute cache) with functionality (disabled once file ID is found).


41-59: LGTM: Efficient cache update after upload.

The mutation correctly updates the query cache with the newly uploaded file, preventing unnecessary refetches.


94-106: LGTM: Reasonable file matching heuristic.

The matching logic using filename, mimetype, and size provides a good balance between accuracy and simplicity for detecting whether a file already exists in Drive.


108-137: LGTM: Accessible and well-structured component rendering.

The component properly handles all states (idle, uploading, success, error) with appropriate accessibility attributes and user feedback.

src/backend/core/api/viewsets/blob.py (2)

18-18: LGTM!

The import of utils is properly added alongside the existing permissions import from the same package.


167-176: Good error handling with explicit status codes.

The refactored code properly delegates blob retrieval to the utility function and maps exceptions to appropriate HTTP status codes. The error handling is clear and follows REST conventions.

However, consider whether including str(e) in error responses could leak sensitive information. For production, you may want to sanitize error messages or use generic messages for security.

src/backend/core/tests/api/test_config.py (1)

56-68: LGTM! Test properly validates the new file_url field.

The test correctly verifies that:

  1. The file_url is accepted in the configuration
  2. The absolute URL is properly constructed by concatenating base_url and file_url
  3. The response includes the expected file_url value
src/backend/core/urls.py (2)

12-12: LGTM! Import follows project conventions.

The import of DriveAPIView is consistent with the existing pattern for importing viewsets in this file.


214-218: LGTM! Clear and RESTful URL pattern.

The new Drive API endpoint follows project conventions:

  • Uses versioned API path with settings.API_VERSION
  • Clear /interop/ prefix indicates external service integration
  • Named route enables reverse URL lookups
  • Follows the same pattern as other API endpoints in this file

Based on learnings.

src/backend/core/api/utils.py (1)

10-10: LGTM! Import is necessary for the new utility function.

The import of models from core is required for the new get_attachment_from_blob_id function.

src/backend/core/api/viewsets/config.py (1)

53-59: LGTM! Consistent Drive configuration extension.

The file_url field is properly added to both:

  1. The OpenAPI schema definition (lines 53-59)
  2. The runtime response construction (line 114)

The implementation follows the same pattern as sdk_url and api_url, ensuring consistency across the DRIVE configuration.

Also applies to: 114-114

src/backend/core/api/serializers.py (1)

1474-1494: LGTM! Clean serializer for external Drive data.

The PartialDriveItemSerializer is appropriately designed as:

  1. A validation-only serializer (not a ModelSerializer) since it represents external Drive resources
  2. All fields are read-only, which is correct for external API responses
  3. The no-op create() and update() methods prevent misuse
  4. Clear docstring explains its purpose

The serializer follows Django REST Framework best practices for representing external API data.

src/backend/messages/settings.py (1)

745-752: LGTM!

The Drive configuration extension with file_url is appropriate for the interoperability feature. The path structure aligns with the Drive service API design.

src/backend/core/api/viewsets/drive.py (2)

1-32: LGTM!

The imports and class definition are appropriate. Using permissions.IsAuthenticated ensures proper access control, and the refresh token decorator handles token lifecycle management.


128-148: LGTM!

The blob_id validation and attachment retrieval logic properly handles missing and invalid inputs with appropriate error responses.

src/backend/core/tests/authentication/test_backends.py (4)

11-12: LGTM!

The new imports support the session token storage testing functionality.


89-118: LGTM!

The test correctly validates that when email fallback is disabled but duplicates are allowed, a new user is created for a different sub even if the email matches an existing user.


120-152: LGTM!

The test correctly validates that when both email fallback and duplicate emails are disabled, attempting to authenticate with a different sub but matching email raises a SuspiciousOperation and prevents user creation.


409-458: LGTM!

The test comprehensively validates that OIDC tokens are correctly stored in the session after authentication, with proper mocking of external OIDC endpoints.

src/backend/core/tests/api/test_drive.py (5)

24-95: LGTM!

The test fixtures are well-designed and provide appropriate setup for Drive API testing. The mailbox_with_message fixture creates realistic test data with proper access controls and a valid MIME message with attachment.


111-115: LGTM!

The anonymous access tests correctly validate that authentication is required for both GET and POST endpoints.


117-142: LGTM!

The token refresh tests correctly validate that the refresh_oidc_access_token decorator is invoked, ensuring token freshness checks are performed.


143-314: LGTM!

The GET method tests provide comprehensive coverage of success and error scenarios, including workspace resolution, file filtering, and proper validation of external API interactions.


641-710: Verify S3 upload failure handling behavior.

The test test_api_interop_drive_post_upload_to_s3_fails expects an HTTPError to be raised when S3 upload fails, which means the error propagates to the client as a 500 error. This aligns with the current implementation's lack of error handling, but consider whether the view should catch and return a more user-friendly error response.

Otherwise, the POST method tests provide excellent coverage of validation, access control, success flow, and error scenarios.

Is the current S3 failure behavior (letting HTTPError propagate) intentional, or should the view catch this and return a structured error response?

src/backend/core/authentication/backends.py (3)

10-12: LGTM: Library migration to django-lasuite

The import change from mozilla-django-oidc to django-lasuite is clean and properly aliased.


27-32: LGTM: Inheritance updated for LaSuite integration

The class inheritance correctly reflects the library migration.


92-96: LGTM: Extra claims extraction is clear

The method provides a clean extension point for adding custom claims. The structure is straightforward and maintainable.

Comment on lines +73 to +104
def get_attachment_from_blob_id(blob_id, user):
"""
Parse a given blob ID to get the attachment data from the related message raw mime.
Blob IDs in the form msg_[message_id]_[attachment_number] are looked up
directly in the message's attachments.
"""
if not blob_id.startswith("msg_"):
raise ValueError("Invalid blob ID")

message_id = blob_id.split("_")[1]
attachment_number = blob_id.split("_")[2]
message = models.Message.objects.get(id=message_id)

# Does the user have access to the message via its thread?
if not models.ThreadAccess.objects.filter(
thread=message.thread, mailbox__accesses__user=user
).exists():
raise models.Blob.DoesNotExist()

# Does the message have any attachments?
if not message.has_attachments:
raise models.Blob.DoesNotExist()

# Parse the raw mime message to get the attachment
parsed_email = message.get_parsed_data()
attachment = parsed_email.get("attachments", [])[int(attachment_number)]

if not attachment:
raise models.Blob.DoesNotExist()

return attachment

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for edge cases and invalid inputs.

The function has several error handling gaps that could cause uncaught exceptions:

  1. Line 82-83: split("_") assumes exactly 3 parts. Add validation:

    parts = blob_id.split("_")
    if len(parts) != 3:
        raise ValueError("Invalid blob ID format")
    message_id = parts[1]
    attachment_number = parts[2]
  2. Line 83: Validate attachment_number is a valid integer before using it:

    try:
        attachment_idx = int(attachment_number)
    except ValueError:
        raise ValueError("Invalid attachment number")
  3. Line 84: Message.objects.get(id=message_id) can raise Message.DoesNotExist. Either catch it or let it propagate (document this behavior).

  4. Line 98: Array indexing can raise IndexError if attachment_number >= len(attachments). Add bounds checking:

    attachments = parsed_email.get("attachments", [])
    if not attachments or attachment_idx >= len(attachments):
        raise models.Blob.DoesNotExist()
    attachment = attachments[attachment_idx]
  5. Line 100-101: The if not attachment check is redundant if you add proper bounds checking above.

Apply these improvements to make the function more robust:

 def get_attachment_from_blob_id(blob_id, user):
     """
     Parse a given blob ID to get the attachment data from the related message raw mime.
     Blob IDs in the form msg_[message_id]_[attachment_number] are looked up
     directly in the message's attachments.
     """
     if not blob_id.startswith("msg_"):
         raise ValueError("Invalid blob ID")
 
-    message_id = blob_id.split("_")[1]
-    attachment_number = blob_id.split("_")[2]
+    parts = blob_id.split("_")
+    if len(parts) != 3:
+        raise ValueError("Invalid blob ID format")
+    
+    message_id = parts[1]
+    try:
+        attachment_idx = int(parts[2])
+    except ValueError:
+        raise ValueError("Invalid attachment number") from None
+    
+    try:
-        message = models.Message.objects.get(id=message_id)
+        message = models.Message.objects.get(id=message_id)
+    except models.Message.DoesNotExist:
+        raise models.Blob.DoesNotExist() from None
 
     # Does the user have access to the message via its thread?
     if not models.ThreadAccess.objects.filter(
         thread=message.thread, mailbox__accesses__user=user
     ).exists():
         raise models.Blob.DoesNotExist()
 
     # Does the message have any attachments?
     if not message.has_attachments:
         raise models.Blob.DoesNotExist()
 
     # Parse the raw mime message to get the attachment
     parsed_email = message.get_parsed_data()
-    attachment = parsed_email.get("attachments", [])[int(attachment_number)]
-
-    if not attachment:
+    attachments = parsed_email.get("attachments", [])
+    if attachment_idx >= len(attachments):
         raise models.Blob.DoesNotExist()
+    
+    attachment = attachments[attachment_idx]
 
     return attachment
🤖 Prompt for AI Agents
In src/backend/core/api/utils.py around lines 73 to 104, the function
get_attachment_from_blob_id assumes blob_id splits into three parts, assumes
attachment_number is an int, and indexes attachments without bounds checks;
update it to validate the split parts length and raise ValueError for invalid
format, parse and validate attachment_number as an integer (raising ValueError
on failure), optionally handle Message.DoesNotExist (or document it), retrieve
parsed_email.get("attachments", []) into a local variable and check that the
list exists and the attachment index is within bounds (raise
models.Blob.DoesNotExist on out-of-range or empty list), and remove the
redundant final truthiness check for attachment since bounds checking covers
that.

Comment on lines +78 to +95
response = requests.get(
f"{drive_external_api}/items/",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=5,
)
response.raise_for_status()
data = response.json()
items = data.get("results", [])
main_workspace = None
for item in items:
if item["main_workspace"]:
main_workspace = item
break
if not main_workspace:
return Response(status=404, data={"error": "No main workspace found"})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for external API calls and consider extracting workspace retrieval.

The external Drive API calls lack error handling for network failures and HTTP errors. If requests.get() fails or raise_for_status() throws an exception, the user will receive an unhelpful 500 error.

Additionally, the workspace retrieval logic (lines 78-95) is duplicated in the POST method (lines 151-172). Consider extracting this into a helper method.

Apply this diff to add error handling:

+from rest_framework.exceptions import APIException
+
+class DriveAPIError(APIException):
+    """Exception for Drive API errors."""
+    status_code = 502
+    default_detail = "Error communicating with Drive service"
+
 class DriveAPIView(APIView):
     ...
     
     def get(self, request):
         """
         Search for files created by the current user.
         """
         drive_external_api = (
             f"{settings.DRIVE_CONFIG.get('base_url')}/external_api/v1.0"
         )
         access_token = request.session.get("oidc_access_token")

         filters = {
             "is_creator_me": True,
             "type": "file",
         }
         if title := request.query_params.get("title"):
             filters.update({"title": title})

-        # Retrieve the main workspace
-        response = requests.get(
-            f"{drive_external_api}/items/",
-            headers={
-                "Authorization": f"Bearer {access_token}",
-                "Content-Type": "application/json",
-            },
-            timeout=5,
-        )
-        response.raise_for_status()
+        try:
+            # Retrieve the main workspace
+            response = requests.get(
+                f"{drive_external_api}/items/",
+                headers={
+                    "Authorization": f"Bearer {access_token}",
+                    "Content-Type": "application/json",
+                },
+                timeout=5,
+            )
+            response.raise_for_status()
+        except requests.exceptions.RequestException as exc:
+            raise DriveAPIError(f"Failed to retrieve workspaces: {exc}") from exc

Consider extracting workspace retrieval to a helper method to reduce duplication with the POST method.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/core/api/viewsets/drive.py around lines 78-95, the external
requests.get call and raise_for_status are unprotected and workspace lookup
logic is duplicated; wrap the HTTP call in a try/except that catches
requests.exceptions.RequestException and requests.exceptions.HTTPError (and
specifically handle timeouts) so you return a clear Response to the client (e.g.
502 with a JSON {"error": "...", "details": str(e)} for network/HTTP errors, 504
for timeouts) and log the exception; then extract the for-loop that finds
item["main_workspace"] into a private helper (e.g.
_get_main_workspace(access_token) or similar) that encapsulates the request,
error handling, and returns the main workspace or None (or raises a controlled
exception) so both GET and POST can reuse it instead of duplicating lines 78-95.

Comment on lines +97 to +108
# Search for files at the root of the main workspace
response = requests.get(
f"{drive_external_api}/items/{main_workspace['id']}/children/",
params=filters,
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=5,
)

return Response(response.json())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling for the file search API call.

Similar to the workspace retrieval, this external API call lacks error handling. Wrap this in a try-except block to provide meaningful error messages to clients.

Apply this diff:

-        # Search for files at the root of the main workspace
-        response = requests.get(
-            f"{drive_external_api}/items/{main_workspace['id']}/children/",
-            params=filters,
-            headers={
-                "Authorization": f"Bearer {access_token}",
-                "Content-Type": "application/json",
-            },
-            timeout=5,
-        )
-
-        return Response(response.json())
+        try:
+            # Search for files at the root of the main workspace
+            response = requests.get(
+                f"{drive_external_api}/items/{main_workspace['id']}/children/",
+                params=filters,
+                headers={
+                    "Authorization": f"Bearer {access_token}",
+                    "Content-Type": "application/json",
+                },
+                timeout=5,
+            )
+            response.raise_for_status()
+        except requests.exceptions.RequestException as exc:
+            raise DriveAPIError(f"Failed to search files: {exc}") from exc
+
+        return Response(response.json())

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/core/api/viewsets/drive.py around lines 97 to 108, the GET call
to the external file search (requests.get(...)) lacks error handling and
directly returns response.json(); wrap the call in a try/except to catch
requests.exceptions.RequestException and JSON decode errors, check
response.status_code and raise or return a DRF Response with a meaningful error
message and appropriate HTTP status (e.g., 502 for upstream errors or 504 for
timeouts), and log the exception; ensure timeouts are handled specifically and
include the underlying exception message in the response body for debugging
while avoiding sensitive data.

Comment on lines +150 to +189
# Get the main workspace
response = requests.get(
f"{drive_external_api}/items/",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=5,
)
response.raise_for_status()
data = response.json()
items = data["results"]
main_workspace = None
for item in items:
if item["main_workspace"]:
main_workspace = item
break

if not main_workspace:
return Response(
status=status.HTTP_404_NOT_FOUND,
data={"error": "No main workspace found"},
)

# Create a new file in the main workspace
response = requests.post(
f"{drive_external_api}/items/{main_workspace['id']}/children/",
json={
"type": "file",
"filename": attachment["name"],
},
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=5,
)
response.raise_for_status()
item = response.json()
policy = item["policy"]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling and validate attachment structure.

Similar error handling issues as the GET method. Additionally, the code assumes attachment has a "name" key without validation. If the attachment structure is unexpected, this will raise a KeyError.

Apply this diff:

+        if "name" not in attachment:
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"error": "Attachment missing required 'name' field"},
+            )
+
-        # Get the main workspace
-        response = requests.get(
-            f"{drive_external_api}/items/",
-            headers={
-                "Authorization": f"Bearer {access_token}",
-                "Content-Type": "application/json",
-            },
-            timeout=5,
-        )
-        response.raise_for_status()
+        try:
+            # Get the main workspace
+            response = requests.get(
+                f"{drive_external_api}/items/",
+                headers={
+                    "Authorization": f"Bearer {access_token}",
+                    "Content-Type": "application/json",
+                },
+                timeout=5,
+            )
+            response.raise_for_status()
+        except requests.exceptions.RequestException as exc:
+            raise DriveAPIError(f"Failed to retrieve workspaces: {exc}") from exc
         
         # ... (similar wrapping for file creation call)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/core/api/viewsets/drive.py around lines 150 to 189, the POST call
that creates a file assumes attachment["name"] exists and has no error handling
for request failures; validate that attachment is a dict with a non-empty "name"
string and return a 400 Response if invalid, wrap the requests.post call in a
try/except catching requests.exceptions.RequestException (including Timeout) and
call response.raise_for_status inside the try, and on exception return a
Response with an appropriate status (e.g., 502 for upstream errors or 504 for
timeout) including the error message; after a successful POST, validate the
returned JSON contains the expected "policy" key and return a 500/502-style
Response if missing.

Comment on lines +191 to +211
# Upload file content using the presigned URL
upload_response = requests.put(
policy,
data=attachment["content"],
headers={"Content-Type": attachment["type"], "x-amz-acl": "private"},
timeout=60,
)
upload_response.raise_for_status()

# Tell the Drive API that the upload is ended
response = requests.post(
f"{drive_external_api}/items/{item['id']}/upload-ended/",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
timeout=5,
)
response.raise_for_status()

return Response(response.json())
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Increase timeout for large file uploads and add error handling.

The 60-second timeout may be insufficient for large attachments (up to 5GB per ITEM_FILE_MAX_SIZE setting). Additionally, the code assumes attachment contains "content" and "type" keys without validation, and lacks error handling for the S3 upload and completion calls.

Apply this diff:

+        # Validate attachment structure
+        required_fields = ["content", "type", "name"]
+        missing_fields = [f for f in required_fields if f not in attachment]
+        if missing_fields:
+            return Response(
+                status=status.HTTP_400_BAD_REQUEST,
+                data={"error": f"Attachment missing required fields: {missing_fields}"},
+            )
+
-        # Upload file content using the presigned URL
-        upload_response = requests.put(
-            policy,
-            data=attachment["content"],
-            headers={"Content-Type": attachment["type"], "x-amz-acl": "private"},
-            timeout=60,
-        )
-        upload_response.raise_for_status()
+        try:
+            # Upload file content using the presigned URL
+            # Increase timeout for large files (5MB/s assumed: 5GB takes ~1000s)
+            upload_response = requests.put(
+                policy,
+                data=attachment["content"],
+                headers={"Content-Type": attachment["type"], "x-amz-acl": "private"},
+                timeout=300,  # 5 minutes
+            )
+            upload_response.raise_for_status()
+        except requests.exceptions.RequestException as exc:
+            raise DriveAPIError(f"Failed to upload file to storage: {exc}") from exc

-        # Tell the Drive API that the upload is ended
-        response = requests.post(
-            f"{drive_external_api}/items/{item['id']}/upload-ended/",
-            headers={
-                "Authorization": f"Bearer {access_token}",
-                "Content-Type": "application/json",
-            },
-            timeout=5,
-        )
-        response.raise_for_status()
+        try:
+            # Tell the Drive API that the upload is ended
+            response = requests.post(
+                f"{drive_external_api}/items/{item['id']}/upload-ended/",
+                headers={
+                    "Authorization": f"Bearer {access_token}",
+                    "Content-Type": "application/json",
+                },
+                timeout=5,
+            )
+            response.raise_for_status()
+        except requests.exceptions.RequestException as exc:
+            raise DriveAPIError(f"Failed to complete upload: {exc}") from exc
🤖 Prompt for AI Agents
In src/backend/core/api/viewsets/drive.py around lines 191 to 211, the presigned
PUT uses a short fixed timeout and assumes attachment has "content" and "type"
keys with no error handling; increase the upload timeout to a larger value (e.g.
300 seconds or configurable via settings), validate that attachment contains
"content" and "type" before attempting the upload (return a 400 Response if
missing), and wrap both the upload (requests.put) and completion (requests.post)
calls in try/except to catch requests.exceptions.RequestException; on exception,
log the error with context and return an appropriate Response with the remote
status code or 502/504 and a clear error message, and call raise_for_status only
when inside the try block so exceptions are handled uniformly.

Comment on lines +3 to +6
from django.urls import include, path

urlpatterns = [
# Override the default 'logout/' path from Mozilla Django OIDC with our custom view.
path("logout/", OIDCLogoutView.as_view(), name="oidc_logout_custom"),
path(
"logout-callback/",
OIDCLogoutCallbackView.as_view(),
name="oidc_logout_callback",
),
*mozzila_oidc_urls,
path("", include("lasuite.oidc_login.urls")),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that all OIDC endpoints are provided by django-lasuite.

The migration from explicit mozilla-django-oidc URL patterns to include("lasuite.oidc_login.urls") simplifies the configuration, but you should verify that all required OIDC endpoints are still accessible:

  1. Login/Authorization endpoint - typically /oidc/authenticate/
  2. Callback endpoint - typically /oidc/callback/
  3. Logout endpoint - previously explicitly configured
  4. Logout callback endpoint - previously explicitly configured

Run the following script to inspect what URLs are provided by django-lasuite:

Additionally, ask the user to search for the latest django-lasuite documentation:


🏁 Script executed:

#!/bin/bash
# Check what URLs are provided by django-lasuite

echo "Checking django-lasuite package for URL patterns..."
python -c "
try:
    from lasuite.oidc_login import urls
    import django
    from django.urls import URLPattern, URLResolver
    
    def print_urls(urlpatterns, prefix=''):
        for pattern in urlpatterns:
            if isinstance(pattern, URLResolver):
                print_urls(pattern.url_patterns, prefix + str(pattern.pattern))
            elif isinstance(pattern, URLPattern):
                print(f'{prefix}{pattern.pattern} -> {pattern.callback.__name__ if hasattr(pattern.callback, \"__name__\") else pattern.callback}')
    
    print('URLs provided by lasuite.oidc_login:')
    print_urls(urls.urlpatterns)
except Exception as e:
    print(f'Error: {e}')
"

Length of output: 84


🌐 Web query:

What authentication URLs and endpoints does django-lasuite provide through lasuite.oidc_login.urls?

💡 Result:

The lasuite.oidc_login.urls module exposes the standard mozilla-django-oidc OIDC login/logout endpoints — i.e. the OIDC "init", "callback" and "logout" routes (the same URL names used by mozilla-django-oidc: oidc_authentication_init, oidc_authentication_callback and oidc_logout). These are intended to be included under a prefix you choose (e.g. include('lasuite.oidc_login.urls') mounted at /oidc/ will give you the init at /oidc/ (login/init), the callback at /oidc/callback/ and logout at /oidc/logout/ — matching mozilla-django-oidc's views and names. [1][2]

Sources:

  • lasuite (django-lasuite) repository (routes delegate to the Mozilla OIDC integration). [1]
  • mozilla-django-oidc documentation (describes the endpoint view names and callback path expectations: oidc_authentication_init, oidc_authentication_callback, oidc_logout). [2]

References:
[1] suitenumerique/django-lasuite repository.
[2] mozilla-django-oidc docs (installation/settings).


🏁 Script executed:

#!/bin/bash
# Check the previous state of the urls.py file to see what was removed

echo "Checking git history for src/backend/core/authentication/urls.py..."
git log --oneline -5 src/backend/core/authentication/urls.py 2>/dev/null || echo "Not a git repo or file not tracked"

echo -e "\n--- Current state of the file ---"
cat src/backend/core/authentication/urls.py 2>/dev/null || echo "File not found"

echo -e "\n--- Checking for any other OIDC-related configuration in the directory ---"
find src/backend/core/authentication -type f -name "*.py" -exec grep -l "oidc\|logout" {} \;

Length of output: 527


🏁 Script executed:

#!/bin/bash
# Show previous version of the file and inspect backends.py

echo "--- Previous version of urls.py (before the refactor) ---"
git show 53db5a7:src/backend/core/authentication/urls.py 2>/dev/null || echo "Could not retrieve previous version"

echo -e "\n--- Current backends.py ---"
cat src/backend/core/authentication/backends.py 2>/dev/null || echo "File not found"

echo -e "\n--- Searching for OIDC/logout related settings in settings files ---"
find src/backend -name "settings*.py" -o -name "config*.py" | head -5 | xargs grep -l "OIDC\|oidc\|logout" 2>/dev/null || echo "No settings found with OIDC config"

Length of output: 7291


🏁 Script executed:

#!/bin/bash
# Search for the custom logout views that were previously imported

echo "--- Searching for OIDCLogoutView and OIDCLogoutCallbackView definitions ---"
find src/backend -name "views.py" -type f -exec grep -l "OIDCLogoutView\|OIDCLogoutCallbackView" {} \;

echo -e "\n--- Checking if views.py exists in authentication module ---"
ls -la src/backend/core/authentication/views.py 2>/dev/null || echo "views.py not found"

echo -e "\n--- If views.py exists, show full content ---"
if [ -f src/backend/core/authentication/views.py ]; then
    cat src/backend/core/authentication/views.py
fi

echo -e "\n--- Check for any imports of views in the module ---"
rg -n "OIDCLogoutView|OIDCLogoutCallbackView" src/backend/core/authentication/ 2>/dev/null || echo "No matches found"

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Show the full diff for the refactor commit to see what changed

echo "--- Git diff for the refactor commit (9e0f7f3) ---"
git show 9e0f7f3 --stat | head -20

echo -e "\n--- Full diff for authentication module ---"
git show 9e0f7f3 -- src/backend/core/authentication/ || echo "Could not show diff"

echo -e "\n--- Check if views were deleted in this commit ---"
git show 9e0f7f3 -- src/backend/core/authentication/views.py 2>/dev/null || echo "views.py was deleted or not part of this commit"

Length of output: 20177


🌐 Web query:

django-lasuite OIDC logout state verification callback handling

💡 Result:

Short answer

  • django-lasuite itself is a small wrapper that delegates OIDC behaviour to a standard OIDC client library (it depends on mozilla‑django‑oidc / other OIDC libs). See the package metadata. [1]
  • RP‑initiated logout follows the OpenID Connect RP‑Initiated Logout spec: the logout request may include id_token_hint, post_logout_redirect_uri and an optional state value which the OP must return when redirecting back to the RP. The RP must verify that returned state matches the one it originally sent (or else reject the callback). [2][3]

What that means in practice for django-lasuite

  • django-lasuite will not magically handle a logout-state CSRF check for you unless the underlying OIDC library/view does. Implementations typically:
    1. generate a secure random state and store it in the user’s session (or a server-side cache) when redirecting the browser to the OP end_session_endpoint;
    2. include that state in the logout URL as the state param;
    3. when the OP redirects to post_logout_redirect_uri, compare the returned state to the stored one and only accept the callback if they match.
  • You must ensure session persistence across the logout redirect (shared cache/session store if you run multiple instances), otherwise the stored state will be missing and you’ll see “state mismatch / None” problems. [2][4]

Actionable checklist

  • Confirm which OIDC backend your project is using (mozilla-django-oidc / django-pyoidc / others). django-lasuite’s PyPI metadata shows its dependencies. [1]
  • If using mozilla-django-oidc (or similar), confirm its logout view behavior and whether it stores/validates state; if not, implement the three-step pattern above in your logout initiation and callback view. [2]
  • Register your post_logout_redirect_uri with the OP and send id_token_hint or client_id so the OP can validate the redirect and return state. [3]

References

  • django-lasuite (package / metadata). [1]
  • mozilla-django-oidc / OIDC logout configuration and common behavior (see docs / library). [2]
  • OpenID Connect RP‑Initiated Logout spec (state, post_logout_redirect_uri, id_token_hint). [3]
  • Notes on session/state issues and persistence across instances (common troubleshooting). [4]

🏁 Script executed:

#!/bin/bash
# Show the deleted test files to understand what was tested

echo "--- Deleted test_views.py content (via git) ---"
git show 9e0f7f3^:src/backend/core/tests/authentication/test_views.py 2>/dev/null | head -100

echo -e "\n--- Deleted parts of test_urls.py (via git) ---"
git show 9e0f7f3^:src/backend/core/tests/authentication/test_urls.py 2>/dev/null

echo -e "\n--- Check if new tests for lasuite integration exist ---"
find src/backend/core/tests -name "*lasuite*" -o -name "*oidc*" -type f | head -10

Length of output: 3519


Critical security issue: Custom OIDC logout state validation has been removed without verified replacement.

The refactor deleted the custom logout views (OIDCLogoutView, OIDCLogoutCallbackView) that implemented OIDC RP-Initiated Logout state generation and verification—a security-critical component to prevent CSRF attacks during logout. The deleted code (137 lines) handled:

  • State persistence across logout redirect
  • State verification on logout callback
  • Session cleanup with state validation

django-lasuite does not automatically provide state validation for logout callbacks; this requires explicit implementation. All 231 lines of tests for this functionality were also deleted with no replacement tests added.

Verify that:

  1. django-lasuite's lasuite.oidc_login.urls includes equivalent state validation for the logout callback endpoint
  2. Tests exist (either new or inherited from django-lasuite) that verify state is properly validated during logout callback
  3. The OIDC configuration in settings includes logout endpoint and state parameters

Without this verification, logout state validation may be broken, creating a security vulnerability.

Comment on lines +629 to 644
# Store OIDC tokens in the session
OIDC_STORE_ACCESS_TOKEN = values.BooleanValue(
default=False,
environ_name="OIDC_STORE_ACCESS_TOKEN",
environ_prefix=None,
) # Store the access token in the session
OIDC_STORE_REFRESH_TOKEN = values.BooleanValue(
default=False,
environ_name="OIDC_STORE_REFRESH_TOKEN",
environ_prefix=None,
) # Store the encrypted refresh token in the session

# Required for refresh token encryption
OIDC_STORE_REFRESH_TOKEN_KEY = values.Value(
None, environ_name="OIDC_STORE_REFRESH_TOKEN_KEY", environ_prefix=None
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation for OIDC_STORE_REFRESH_TOKEN_KEY when refresh token storage is enabled.

When OIDC_STORE_REFRESH_TOKEN=True, the OIDC_STORE_REFRESH_TOKEN_KEY is required for encryption but currently defaults to None. This could cause runtime failures. Consider adding a validation check in post_setup() to ensure the key is set when refresh token storage is enabled.

Apply this diff to add validation:

         if (
             cls.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
             and cls.OIDC_ALLOW_DUPLICATE_EMAILS
         ):
             raise ValueError(
                 "Both OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION and "
                 "OIDC_ALLOW_DUPLICATE_EMAILS cannot be set to True simultaneously. "
             )
+
+        if cls.OIDC_STORE_REFRESH_TOKEN and not cls.OIDC_STORE_REFRESH_TOKEN_KEY:
+            raise ValueError(
+                "OIDC_STORE_REFRESH_TOKEN_KEY must be set when "
+                "OIDC_STORE_REFRESH_TOKEN is enabled."
+            )

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/backend/messages/settings.py around lines 629 to 644, add a validation in
the class's post_setup() (or equivalent finalization) that checks: if
OIDC_STORE_REFRESH_TOKEN is True and OIDC_STORE_REFRESH_TOKEN_KEY is None or
empty, raise a clear configuration error (e.g., ValueError or
ImproperlyConfigured) explaining that OIDC_STORE_REFRESH_TOKEN_KEY must be set
when refresh token storage is enabled; implement the check early in post_setup()
so misconfiguration fails fast with an explicit message.

Comment on lines +165 to +167
img {
max-width: 100vw;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use 100% instead of 100vw to constrain images to the iframe.

Inside an iframe, 100vw refers to the entire browser viewport width, not the iframe's width. If the iframe is narrower than the viewport, images could overflow the iframe container, causing layout issues or clipping.

Apply this diff to constrain images to the iframe's width:

                 img {
-                    max-width: 100vw;
+                    max-width: 100%;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
img {
max-width: 100vw;
}
img {
max-width: 100%;
}
🤖 Prompt for AI Agents
In
src/frontend/src/features/layouts/components/thread-view/components/thread-message/message-body.tsx
around lines 165 to 167, the CSS rule uses max-width: 100vw which measures the
full browser viewport and can make images overflow when rendered inside an
iframe; change that rule to use max-width: 100% so images are constrained to
their container (the iframe/content box) instead of the viewport, updating the
CSS selector accordingly.

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