-
Notifications
You must be signed in to change notification settings - Fork 7
Feat/drive interop save attachment #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughMigrates 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
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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20–30 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.pysrc/backend/core/api/utils.pysrc/backend/core/api/viewsets/config.py
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. Comment |
61005ab to
fccc491
Compare
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
fccc491 to
bf29028
Compare
There was a problem hiding this 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 attachmentidfield removal.The search confirms this is indeed a breaking change:
Frontend code uses
attachment.idin two locations:
src/frontend/src/features/utils/mail-helper.tsx:163- usesa.idindata-idattributesrc/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx:87- usesattachment.idasfileIdparameter toDrivePreviewLinkCurrent serializer does not expose
id:
- The
AttachmentSerializer.Meta.fieldslist (lines 374-399) does not include"id"- Only exposes:
blobId,name,size,type,sha256,created_at,cidRequired actions before merge:
- Update frontend code in both locations to use an alternative identifier (likely
blobIdinstead ofid)- 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 inget_or_create_user(lines 51, 78)
Line 51:
get_userinfo(access_token, id_token, payload)passes wrong parameter. Parent method signature expectsget_userinfo(self, access_token, id_token=None, userinfo_endpoint=None). Replacepayloadwithuserinfo_endpointor remove the third argument if not needed.Line 78:
self.update_user_if_needed(user, claims)calls undefined method. Parent class providesupdate_user(self, user, claims), notupdate_user_if_needed. Either rename toupdate_useror 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:
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_TOKENisTrue- A reference to how to generate a valid Fernet key
- A security warning about key rotation and storage
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_urlis a URL; declare"format": "uri"for validation. Consider adding the same tosdk_urlandapi_urlfor 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 usesinterop. 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 withtitle, 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 requirecountandresults.- Add
"format": "uri"tonext/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 eventsPer 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_createdflag appears unused in the post-hookThe
_user_createdflag is passed topost_get_or_create_userbut 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-hookThe
claimsand_user_createdparameters 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 scenarioThe method correctly wraps the
DuplicateEmailErroras aSuspiciousOperationand 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
⛔ Files ignored due to path filters (11)
src/backend/poetry.lockis excluded by!**/*.locksrc/frontend/src/features/api/gen/index.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/interop-drive/interop-drive.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/interop/interop.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/attachment.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/drive_upload_attachment_request.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/index.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/interop_drive_retrieve_params.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/paginated_drive_item_response.tsis excluded by!**/gen/**src/frontend/src/features/api/gen/models/partial_drive_item.tsis 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.pysrc/backend/core/tests/api/test_config.pysrc/backend/core/api/viewsets/blob.pysrc/backend/core/authentication/urls.pysrc/backend/core/tests/authentication/test_backends.pysrc/backend/core/api/viewsets/drive.pysrc/backend/core/tests/api/test_drive.pysrc/backend/core/api/utils.pysrc/backend/core/urls.pysrc/backend/core/api/serializers.pysrc/backend/core/authentication/backends.pysrc/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.pysrc/backend/core/tests/authentication/test_backends.pysrc/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.pysrc/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.pysrc/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:
- Stability: Version 0.0.14 is pre-release software and may introduce breaking changes in upcoming patches/minors.
- Scope: Installing all extras (
[all]) may pull in unused or conflicting dependencies. Only the specific extras needed for Drive interop should be included.- 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.1has 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
idfield 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
serializeToDriveFilefunction properly maps Drive SDK items to the expected DriveFile structure, including the newidfield.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
utilsis properly added alongside the existingpermissionsimport 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:
- The
file_urlis accepted in the configuration- The absolute URL is properly constructed by concatenating base_url and file_url
- The response includes the expected file_url value
src/backend/core/urls.py (2)
12-12: LGTM! Import follows project conventions.The import of
DriveAPIViewis 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
modelsfromcoreis required for the newget_attachment_from_blob_idfunction.src/backend/core/api/viewsets/config.py (1)
53-59: LGTM! Consistent Drive configuration extension.The
file_urlfield is properly added to both:
- The OpenAPI schema definition (lines 53-59)
- The runtime response construction (line 114)
The implementation follows the same pattern as
sdk_urlandapi_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
PartialDriveItemSerializeris appropriately designed as:
- A validation-only serializer (not a ModelSerializer) since it represents external Drive resources
- All fields are read-only, which is correct for external API responses
- The no-op
create()andupdate()methods prevent misuse- 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_urlis 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.IsAuthenticatedensures 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
subeven 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
subbut matching email raises aSuspiciousOperationand 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_messagefixture 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_tokendecorator 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_failsexpects anHTTPErrorto 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-lasuiteThe import change from mozilla-django-oidc to django-lasuite is clean and properly aliased.
27-32: LGTM: Inheritance updated for LaSuite integrationThe class inheritance correctly reflects the library migration.
92-96: LGTM: Extra claims extraction is clearThe method provides a clean extension point for adding custom claims. The structure is straightforward and maintainable.
| 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 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for edge cases and invalid inputs.
The function has several error handling gaps that could cause uncaught exceptions:
-
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]
-
Line 83: Validate
attachment_numberis a valid integer before using it:try: attachment_idx = int(attachment_number) except ValueError: raise ValueError("Invalid attachment number")
-
Line 84:
Message.objects.get(id=message_id)can raiseMessage.DoesNotExist. Either catch it or let it propagate (document this behavior). -
Line 98: Array indexing can raise
IndexErrorif 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]
-
Line 100-101: The
if not attachmentcheck 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.
| 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"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 excConsider 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.
| # 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()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| # 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"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| # 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()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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:
- Login/Authorization endpoint - typically
/oidc/authenticate/ - Callback endpoint - typically
/oidc/callback/ - Logout endpoint - previously explicitly configured
- 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:
- 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;
- include that state in the logout URL as the state param;
- 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 -10Length 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:
- django-lasuite's
lasuite.oidc_login.urlsincludes equivalent state validation for the logout callback endpoint - Tests exist (either new or inherited from django-lasuite) that verify state is properly validated during logout callback
- The OIDC configuration in settings includes logout endpoint and state parameters
Without this verification, logout state validation may be broken, creating a security vulnerability.
| # 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 | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| img { | ||
| max-width: 100vw; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
Purpose
Install
django-lasuiteand our custom authentication backend to inheritfrom 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
Bug Fixes
Documentation
Chores