Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion src/sentry/seer/autofix/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from sentry.integrations.models.external_actor import ExternalActor
from sentry.integrations.types import ExternalProviders
from sentry.issues.grouptype import WebVitalsGroup
from sentry.models.commitauthor import CommitAuthor
from sentry.models.group import Group
from sentry.models.project import Project
from sentry.search.eap.types import SearchResolverConfig
Expand Down Expand Up @@ -341,7 +342,17 @@ def _respond_with_error(reason: str, status: int):


def _get_github_username_for_user(user: User | RpcUser, organization_id: int) -> str | None:
"""Get GitHub username for a user by checking ExternalActor mappings."""
"""
Get GitHub username for a user by checking multiple sources.

This function attempts to resolve a Sentry user to their GitHub username by:
1. Checking ExternalActor for explicit user→GitHub mappings
2. Falling back to CommitAuthor records matched by email (like suspect commits)
3. Extracting the GitHub username from the CommitAuthor external_id
"""
from sentry.users.services.user.service import user_service

# Method 1: Check ExternalActor for direct user→GitHub mapping
external_actor: ExternalActor | None = (
ExternalActor.objects.filter(
user_id=user.id,
Expand All @@ -359,6 +370,42 @@ def _get_github_username_for_user(user: User | RpcUser, organization_id: int) ->
username = external_actor.external_name
return username[1:] if username.startswith("@") else username

# Method 2: Check CommitAuthor by email matching (like suspect commits does)
# Get all verified emails for this user
user_emails = [user.email] if hasattr(user, "email") and user.email else []
try:
# For RpcUser, get verified emails directly from the object
if hasattr(user, "get_verified_emails"):
verified_emails = user.get_verified_emails()
user_emails.extend([e.email for e in verified_emails])
else:
# For ORM User, fetch from service
user_details = user_service.get_user(user_id=user.id)
if user_details and hasattr(user_details, "get_verified_emails"):
verified_emails = user_details.get_verified_emails()
user_emails.extend([e.email for e in verified_emails])
except Exception:
# If we can't get verified emails, continue with just the primary email
pass

if user_emails:
# Find CommitAuthors with matching emails that have GitHub external_id
commit_author = (
CommitAuthor.objects.filter(
organization_id=organization_id,
email__in=[email.lower() for email in user_emails],
external_id__isnull=False,
)
.exclude(external_id="")
Comment on lines +394 to +399

Choose a reason for hiding this comment

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

Critical severity and reachable issue identified in your code:
Line 394 has a vulnerable usage of django, introducing a critical severity vulnerability.

ℹ️ Why this is reachable

A reachable issue is a real security risk because your project actually executes the vulnerable code. This issue is reachable because your code uses a certain version of django.
Affected versions of django are vulnerable to Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection'). The ORM methods QuerySet.filter(), QuerySet.exclude(), QuerySet.get() and the Q() class can be tricked into SQL injection when you pass a specially crafted dictionary via **kwargs that includes a malicious _connector entry. This bypasses the normal query parameterization and lets an attacker inject arbitrary SQL into the WHERE clause.

References: GHSA, CVE

To resolve this comment:
Upgrade this dependency to at least version 5.2.8 at uv.lock.

💬 Ignore this finding

To ignore this, reply with:

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

You can view more details on this finding in the Semgrep AppSec Platform here.

.order_by("-id")
.first()
)

if commit_author:
commit_username = commit_author.get_username_from_external_id()
if commit_username:
return commit_username

return None


Expand Down
108 changes: 108 additions & 0 deletions tests/sentry/seer/autofix/test_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,114 @@ def test_get_github_username_for_user_multiple_mappings(self) -> None:
username = _get_github_username_for_user(user, organization.id)
assert username == "newuser"

def test_get_github_username_for_user_from_commit_author(self) -> None:
"""Tests getting GitHub username from CommitAuthor when ExternalActor doesn't exist."""
from sentry.models.commitauthor import CommitAuthor

user = self.create_user(email="committer@example.com")
organization = self.create_organization()
self.create_member(user=user, organization=organization)

# Create CommitAuthor with GitHub external_id
CommitAuthor.objects.create(
organization_id=organization.id,
name="Test Committer",
email="committer@example.com",
external_id="github:githubuser",
)

username = _get_github_username_for_user(user, organization.id)
assert username == "githubuser"

def test_get_github_username_for_user_from_commit_author_github_enterprise(self) -> None:
"""Tests getting GitHub Enterprise username from CommitAuthor."""
from sentry.models.commitauthor import CommitAuthor

user = self.create_user(email="committer@company.com")
organization = self.create_organization()
self.create_member(user=user, organization=organization)

# Create CommitAuthor with GitHub Enterprise external_id
CommitAuthor.objects.create(
organization_id=organization.id,
name="Enterprise User",
email="committer@company.com",
external_id="github_enterprise:ghuser",
)

username = _get_github_username_for_user(user, organization.id)
assert username == "ghuser"

def test_get_github_username_for_user_external_actor_priority(self) -> None:
"""Tests that ExternalActor is checked before CommitAuthor."""
from sentry.integrations.models.external_actor import ExternalActor
from sentry.integrations.types import ExternalProviders
from sentry.models.commitauthor import CommitAuthor

user = self.create_user(email="committer@example.com")
organization = self.create_organization()
self.create_member(user=user, organization=organization)

# Create both ExternalActor and CommitAuthor
ExternalActor.objects.create(
user_id=user.id,
organization=organization,
provider=ExternalProviders.GITHUB.value,
external_name="@externaluser",
external_id="ext123",
integration_id=7,
)

CommitAuthor.objects.create(
organization_id=organization.id,
name="Commit User",
email="committer@example.com",
external_id="github:commituser",
)

# Should use ExternalActor (higher priority)
username = _get_github_username_for_user(user, organization.id)
assert username == "externaluser"

def test_get_github_username_for_user_commit_author_no_external_id(self) -> None:
"""Tests that None is returned when CommitAuthor exists but has no external_id."""
from sentry.models.commitauthor import CommitAuthor

user = self.create_user(email="committer@example.com")
organization = self.create_organization()
self.create_member(user=user, organization=organization)

# Create CommitAuthor without external_id
CommitAuthor.objects.create(
organization_id=organization.id,
name="No External ID",
email="committer@example.com",
external_id=None,
)

username = _get_github_username_for_user(user, organization.id)
assert username is None

def test_get_github_username_for_user_wrong_organization(self) -> None:
"""Tests that CommitAuthor from different organization is not used."""
from sentry.models.commitauthor import CommitAuthor

user = self.create_user(email="committer@example.com")
organization1 = self.create_organization()
organization2 = self.create_organization()
self.create_member(user=user, organization=organization1)

# Create CommitAuthor in different organization
CommitAuthor.objects.create(
organization_id=organization2.id,
name="Wrong Org User",
email="committer@example.com",
external_id="github:wrongorguser",
)

username = _get_github_username_for_user(user, organization1.id)
assert username is None


class TestRespondWithError(TestCase):
def test_respond_with_error(self) -> None:
Expand Down
Loading