Skip to content

Add Codecov User Resolution #90411

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

Merged
merged 1 commit into from
Apr 28, 2025
Merged
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
47 changes: 47 additions & 0 deletions src/sentry/codecov/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from dataclasses import dataclass
from typing import Any

from sentry.auth.services.access.service import access_service
from sentry.auth.services.auth import RpcAuthIdentity
from sentry.identity.services.identity import identity_service
from sentry.identity.services.identity.model import RpcIdentity


@dataclass
class CodecovUser:
external_id: str
auth_token: Any


def resolve_codecov_user(user_id: int, organization_id: int) -> CodecovUser | None:
"""Given a Sentry User, and an organization id, find the GitHub user ID and GH access_token
that is linked to Codecov's user.

Choose a reason for hiding this comment

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

"that is linked to Codecov's user" implies Codecov has its own notion of this user's identity. maybe word like "that we can provide in place of a Codecov User"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Technically, we're using the GH id to link it to Codecov's concept of a user (we are still using the owner object in Codecov). The name of the method might be confusing? As we're technically finding the GH user to link to Codecov.

The user resolution will "fall-through" based on whether the Organization has a GitHub Auth Provider enabled.
"""
# Get identity from an AuthProvider if it's available.
auth_provider = access_service.get_auth_provider(organization_id)
if auth_provider and auth_provider.provider == "github":

Choose a reason for hiding this comment

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

are there constants/enums for auth_provider.provider and provider_type values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't seen an enum

auth_identity: RpcAuthIdentity | None = access_service.get_auth_identity_for_user(
auth_provider.id, user_id
)
if auth_identity:
return CodecovUser(
external_id=str(auth_identity.ident),
auth_token=auth_identity.data.get("access_token"),
)

# Get identity from Identity if it's available.
identities: list[RpcIdentity] = identity_service.get_user_identities_by_provider_type(
user_id=user_id, provider_type="github"
)
if identities:
# Note: There is currently either zero or one GitHub "identity" mapped to a user.
identity = identities[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Will we get many identities? And if so, why would we always choose the first? Perhaps a comment to help understand this 👌

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When I checked the database, there should be only one identity mapped to GitHub per user at this point. So, in practice, this is currently zero or one user. I can add a comment for this. 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

gotchaa so this will be like a one off mapping?

return CodecovUser(
external_id=identity.external_id,
auth_token=identity.data.get("access_token"),
)

# No GitHub identities tied to user, return None
return None
Empty file.
43 changes: 43 additions & 0 deletions tests/sentry/codecov/test_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from sentry.codecov.helper import CodecovUser, resolve_codecov_user
from sentry.testutils.cases import TestCase
from sentry.testutils.silo import all_silo_test


@all_silo_test
class TestCodecovUserResolution(TestCase):
def setUp(self):
self.organization = self.create_organization()
self.user = self.create_user()

def test_get_auth_provider_identity_for_user(self):
auth_provider = self.create_auth_provider(
organization_id=self.organization.id, provider="github"
)
self.create_auth_identity(
auth_provider=auth_provider,
user=self.user,
ident=12345,
data={"access_token": "this_is_the_access_token_stored_here"},
)
assert resolve_codecov_user(self.user.id, self.organization.id) == CodecovUser(
external_id="12345", auth_token="this_is_the_access_token_stored_here"
)

def test_get_auth_identity_for_user(self):
# Create the Auth Provider to fall through
self.create_auth_provider(organization_id=self.organization.id, provider="github")

identity_provider = self.create_identity_provider(type="github")
self.create_identity(
user=self.user,
identity_provider=identity_provider,
external_id="identity_id_12345",
data={"access_token": "this_is_the_access_token_stored_here"},
)

assert resolve_codecov_user(self.user.id, self.organization.id) == CodecovUser(
external_id="identity_id_12345", auth_token="this_is_the_access_token_stored_here"
)

def test_no_linked_identity(self):
assert resolve_codecov_user(self.user.id, self.organization.id) is None
Loading