-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
feat(autofix): Add email-based user mapping for Seer Autofix PR review requests #103406
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: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -435,3 +435,112 @@ def get_stacktrace_path_from_event_frame(frame: Mapping[str, Any]) -> str | None | |
| frame: Event frame | ||
| """ | ||
| return frame.get("munged_filename") or frame.get("filename") or frame.get("abs_path") | ||
|
|
||
|
|
||
| def get_github_username_for_user(user: Any, organization_id: int) -> str | None: | ||
| """ | ||
| Get GitHub username for a Sentry 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 | ||
|
|
||
| Args: | ||
| user: Sentry User or RpcUser object | ||
| organization_id: Organization ID to scope the lookup | ||
|
|
||
| Returns: | ||
| GitHub username (without @ prefix) or None if no mapping found | ||
| """ | ||
| import logging | ||
|
|
||
| from sentry.integrations.models.external_actor import ExternalActor | ||
| from sentry.integrations.types import ExternalProviders | ||
| from sentry.models.commitauthor import CommitAuthor | ||
| from sentry.users.services.user.service import user_service | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Method 1: Check ExternalActor for direct user→GitHub mapping | ||
| external_actor: ExternalActor | None = ( | ||
| ExternalActor.objects.filter( | ||
| user_id=user.id, | ||
| organization_id=organization_id, | ||
| provider__in=[ | ||
| ExternalProviders.GITHUB.value, | ||
| ExternalProviders.GITHUB_ENTERPRISE.value, | ||
| ], | ||
| ) | ||
| .order_by("-date_added") | ||
| .first() | ||
| ) | ||
|
|
||
| if external_actor and external_actor.external_name: | ||
| username = external_actor.external_name | ||
| username = username[1:] if username.startswith("@") else username | ||
| logger.info( | ||
| "github_username_resolved_from_external_actor", | ||
| extra={ | ||
| "user_id": user.id, | ||
| "organization_id": organization_id, | ||
| "username": username, | ||
| "source": "external_actor", | ||
| }, | ||
| ) | ||
| return 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="") | ||
|
||
| .order_by("-id") | ||
| .first() | ||
| ) | ||
|
|
||
| if commit_author: | ||
| commit_username = commit_author.get_username_from_external_id() | ||
| if commit_username: | ||
| logger.info( | ||
| "github_username_resolved_from_commit_author", | ||
| extra={ | ||
| "user_id": user.id, | ||
| "organization_id": organization_id, | ||
| "username": commit_username, | ||
| "source": "commit_author", | ||
| }, | ||
| ) | ||
| return commit_username | ||
|
|
||
| logger.info( | ||
| "github_username_not_found", | ||
| extra={ | ||
| "user_id": user.id, | ||
| "organization_id": organization_id, | ||
| "source": "none", | ||
| }, | ||
| ) | ||
| return None | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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="[email protected]") | ||
| 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="[email protected]", | ||
| 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="[email protected]") | ||
| 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="[email protected]", | ||
| 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="[email protected]") | ||
| 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="[email protected]", | ||
| 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="[email protected]") | ||
| 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="[email protected]", | ||
| 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="[email protected]") | ||
| 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="[email protected]", | ||
| 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: | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
Risk: 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_connectorentry. This bypasses the normal query parameterization and lets an attacker inject arbitrary SQL into the WHERE clause.Fix: Upgrade this library to at least version 5.2.8 at sentry/uv.lock:305.
Reference(s): GHSA-frmv-pr5f-9mcr, CVE-2025-64459🎉 Removed in commit d3ede3f 🎉