Skip to content

Dev#128

Merged
ngovinh2k2 merged 20 commits into
mainfrom
dev
Jul 2, 2026
Merged

Dev#128
ngovinh2k2 merged 20 commits into
mainfrom
dev

Conversation

@ngovinh2k2

Copy link
Copy Markdown
Member

What?

Why?

How?

Testing?

  • Functional Testing
  • Security
  • Performance
  • Error Handling
  • Code Quality
  • Documentation
  • Database
  • Deployment
  • Final Review

Anything Else?

ngovinh2k2 and others added 20 commits May 26, 2026 11:39
…ion-cache-after-adding-to-space

fix: clear accepted user permission cache after adding to space
…en-updating-or-deleting-image

fix: remove old image from s3 when updating or deleting image
…e-email-ui-in-the-template-folder

feat: spacedf 20381 be update the email UI in the template folder
…ango-silk-for-api-performance-monitoring-and-profiling

SPACEDF-20702: [SpaceDF] - Implement django silk for API performance monitoring and profiling
…uery-performance

SPACEDF-20711: [SpaceDF] - [BE] Improve query performance
feat: implement skipping migrations if no new migration files
@ngovinh2k2
ngovinh2k2 merged commit 9e05bbc into main Jul 2, 2026
1 check passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several optimizations and features, including custom email template integration via a console service client, optimized space queries using Django subqueries, and the addition of Django Silk for profiling. It also implements cleanup mechanisms for deleted user avatars and space logos. The review feedback highlights a critical Broken Object Level Authorization (BOLA) vulnerability in the space users endpoint, a logic bug where default_display is incorrectly set to False during space creation, performance bottlenecks from synchronous S3 deletions in signal receivers, and transaction safety issues when dispatching Celery tasks before database commits.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread apps/space/views.py


class GetSpaceUsersAPIView(UseTenantFromRequestMixin, APIView):
swagger_schema = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-critical critical

Security Vulnerability: Broken Object Level Authorization (BOLA) / IDOR

The get method in GetSpaceUsersAPIView retrieves and returns all user IDs associated with a space using only the space_slug from the URL parameters:

space = get_object_or_404(Space, slug_name=space_slug)

There is no validation to ensure that the requesting user (identified by the X-User-ID header) actually belongs to or has permission to access the requested space. This allows any authenticated user to query the membership of any space in the system simply by guessing or obtaining its slug.

Recommendation

Verify the user's membership in the requested space before returning the user list. You can leverage the existing get_spaces_queryset_for_user helper function:

    def get(self, request, *args, **kwargs):
        space_slug = kwargs.get("space_slug")
        if not space_slug:
            return Response(
                {"error": "Space slug is required"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        user_id = request.headers.get("X-User-ID", None)
        if not user_id:
            return Response(
                {"error": "User ID is required"},
                status=status.HTTP_401_UNAUTHORIZED,
            )

        # Ensure the user has access to the space
        spaces_qs = get_spaces_queryset_for_user(Space.objects.all(), user_id)
        space = get_object_or_404(spaces_qs, slug_name=space_slug)

        user_ids = (
            OrganizationUser.objects.filter(
                space_role_user__space_role__space_id=space.id
            )
            .values_list("id", flat=True)
            .distinct()
        )

        return Response(
            {
                "user_ids": list(user_ids),
                "total_users": len(user_ids),
            },
            status=status.HTTP_200_OK,
        )

Comment thread apps/space/views.py
Comment on lines +79 to +81
instance.default_display = not SpaceRoleUser.objects.filter(
organization_user_id=organization_user.id
).exists()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Correctness Bug: default_display Will Always Be False

When a new space is created, serializer.save() triggers the post_save signal handle_new_space synchronously. Inside that signal, a SpaceRoleUser membership is automatically created for the user.

Because of this, checking SpaceRoleUser.objects.filter(organization_user_id=organization_user.id).exists() immediately after serializer.save() will always return True, which sets instance.default_display to False even if this was the user's very first space (which should have been marked as default).

To fix this, query the database specifically for the membership of this user in this space and check if it is marked as default.

Suggested change
instance.default_display = not SpaceRoleUser.objects.filter(
organization_user_id=organization_user.id
).exists()
instance.default_display = SpaceRoleUser.objects.filter(
organization_user_id=organization_user.id,
space_role__space=instance,
is_default=True,
).exists()

Comment on lines +11 to +14
delete_file(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.avatar}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Performance Issue: Synchronous Network I/O in Signal Receiver

Calling delete_file synchronously inside a Django signal receiver blocks the request-response cycle. If S3 is slow or down, this will cause the user deletion request to hang or fail.

Since there is already a Celery task delete_upload_file configured for this purpose, you should offload this deletion to Celery using send_task.

Suggested change
delete_file(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.avatar}",
)
from common.celery import constants
from common.celery.task_senders import send_task
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{instance.avatar}",
},
)

Comment thread apps/space/signals.py
Comment on lines +45 to +48
delete_file(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.logo}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Performance Issue: Synchronous Network I/O in Signal Receiver

Calling delete_file synchronously inside a Django signal receiver blocks the request-response cycle. If S3 is slow or down, this will cause the space deletion request to hang or fail.

Since there is already a Celery task delete_upload_file configured for this purpose, you should offload this deletion to Celery using send_task.

Suggested change
delete_file(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.logo}",
)
from common.celery import constants
from common.celery.task_senders import send_task
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{instance.logo}",
},
)

Comment on lines +136 to 143
if old_avatar and old_avatar != new_avatar:
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_avatar}",
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Transaction Safety: Sending Celery Tasks Inside Transactions

Sending a Celery task immediately during a database update can lead to race conditions or data inconsistency. If the database transaction is rolled back after the task is sent, the file will be deleted from S3, but the database will still reference the old avatar.

To prevent this, use transaction.on_commit to ensure the task is only sent after the transaction successfully commits.

Suggested change
if old_avatar and old_avatar != new_avatar:
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_avatar}",
},
)
if old_avatar and old_avatar != new_avatar:
from django.db import transaction
transaction.on_commit(
lambda: send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_avatar}",
},
)
)

Comment thread apps/space/serializers.py
Comment on lines +37 to +44
if old_logo and old_logo != new_logo:
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_logo}",
},
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Transaction Safety: Sending Celery Tasks Inside Transactions

Sending a Celery task immediately during a database update can lead to race conditions or data inconsistency. If the database transaction is rolled back after the task is sent, the file will be deleted from S3, but the database will still reference the old logo.

To prevent this, use transaction.on_commit to ensure the task is only sent after the transaction successfully commits.

Suggested change
if old_logo and old_logo != new_logo:
send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_logo}",
},
)
if old_logo and old_logo != new_logo:
from django.db import transaction
transaction.on_commit(
lambda: send_task(
name=constants.AUTH_SERVICE_DELETE_UPLOAD_FILE,
message={
"bucket_name": settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
"link_file": f"uploads/{old_logo}",
},
)
)

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