Skip to content

Commit 85ce49d

Browse files
wedamijacleptric
authored andcommitted
fix(sync_repos): Handle string JSON in VSTS and error dicts in GitLab (#114656)
Guard against non-dict exc.json in message_from_error and validate that GitLab search_projects returns project dicts, not error keys. Fixes SENTRY-5NAY Fixes SENTRY-5NBH
1 parent c7de9ee commit 85ce49d

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

src/sentry/integrations/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ def message_from_error(self, exc: Exception) -> str:
527527
elif isinstance(exc, UnsupportedResponseType):
528528
return ERR_UNSUPPORTED_RESPONSE_TYPE.format(content_type=exc.content_type)
529529
elif isinstance(exc, ApiError):
530-
if exc.json:
530+
if exc.json and isinstance(exc.json, dict):
531531
msg = self.error_message_from_json(exc.json) or "unknown error"
532532
else:
533533
msg = "unknown error"

src/sentry/integrations/gitlab/integration.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@ def get_repositories(
202202
# Note: gitlab projects are the same things as repos everywhere else
203203
group = self.get_group_id()
204204
resp = self.get_client().search_projects(group, query)
205+
# GitLab returns {"status": "error", ...} when the group is
206+
# inaccessible. The pagination layer turns that dict into a list
207+
# of its keys (e.g. ["status", "error"]), which would crash the
208+
# list comprehension below with TypeError on str["id"].
209+
if resp and not isinstance(resp[0], dict):
210+
raise IntegrationError(
211+
"Expected list of projects from GitLab, got unexpected response"
212+
)
205213
instance = self.model.metadata["instance"]
206214
return [
207215
{

tests/sentry/integrations/source_code_management/test_sync_repos.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sentry.constants import ObjectStatus
1111
from sentry.integrations.github.integration import GitHubIntegrationProvider
1212
from sentry.integrations.github_enterprise.integration import GitHubEnterpriseIntegrationProvider
13+
from sentry.integrations.gitlab.integration import GitlabIntegration
1314
from sentry.integrations.models.organization_integration import OrganizationIntegration
1415
from sentry.integrations.source_code_management.sync_repos import (
1516
sync_repos_for_org,
@@ -572,6 +573,40 @@ def test_creates_new_repos_for_gitlab(self) -> None:
572573
assert len(repos) == 2
573574
assert repos[0].provider == "integrations:gitlab"
574575

576+
@responses.activate
577+
def test_error_dict_response_raises_integration_error(self) -> None:
578+
integration = self.create_provider_integration(
579+
provider="gitlab",
580+
name="Example Gitlab 2",
581+
external_id="example2.gitlab.com:group-y",
582+
metadata={
583+
"instance": "example.gitlab.com",
584+
"base_url": "https://example.gitlab.com",
585+
"domain_name": "example.gitlab.com/group-y",
586+
"verify_ssl": False,
587+
"group_id": 1,
588+
"webhook_secret": "secret123",
589+
},
590+
)
591+
identity = Identity.objects.create(
592+
idp=self.create_identity_provider(type="gitlab", config={}),
593+
user=self.user,
594+
external_id="gitlab456",
595+
data={"access_token": "123456789"},
596+
)
597+
integration.add_organization(self.organization, self.user, identity.id)
598+
installation = integration.get_installation(organization_id=self.organization.id)
599+
600+
responses.add(
601+
responses.GET,
602+
"https://example.gitlab.com/api/v4/groups/1/projects?search=&simple=True&include_subgroups=False&page=1&per_page=100&order_by=last_activity_at",
603+
json={"status": "error", "error": "group not accessible"},
604+
)
605+
606+
assert isinstance(installation, GitlabIntegration)
607+
with pytest.raises(IntegrationError, match="Expected list of projects"):
608+
installation.get_repositories()
609+
575610

576611
@control_silo_test
577612
class SyncReposForOrgBitbucketTestCase(TestCase):
@@ -762,6 +797,21 @@ def _wrap_identity_not_valid(*args: object, **kwargs: object) -> None:
762797
with assume_test_silo_mode(SiloMode.CELL):
763798
assert Repository.objects.count() == 0
764799

800+
def test_vsts_message_from_error_handles_string_json(self) -> None:
801+
integration = self.create_provider_integration(
802+
provider="vsts",
803+
external_id="vsts-account-id-2",
804+
name="MyVSTSAccount",
805+
metadata={"domain_name": "https://myvstsaccount.visualstudio.com/"},
806+
)
807+
integration.add_organization(self.organization, self.user)
808+
installation = integration.get_installation(organization_id=self.organization.id)
809+
810+
exc = ApiForbiddenError("ADO OAuth tokens disabled")
811+
exc.json = "ADO OAuth tokens disabled" # type: ignore[assignment]
812+
msg = installation.message_from_error(exc)
813+
assert "unknown error" in msg
814+
765815

766816
@control_silo_test
767817
class SyncReposForOrgBrokenIdentityTestCase(TestCase):

0 commit comments

Comments
 (0)