PageRenderTime 49ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/src/sentry/integrations/bitbucket/integration.py

https://github.com/getsentry/sentry
Python | 216 lines | 213 code | 3 blank | 0 comment | 0 complexity | 0a7c64a32c4f242f6c18e6c60baf1a37 MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause
  1. from django.utils.datastructures import OrderedSet
  2. from django.utils.translation import ugettext_lazy as _
  3. from rest_framework.request import Request
  4. from rest_framework.response import Response
  5. from sentry.identity.pipeline import IdentityProviderPipeline
  6. from sentry.integrations import (
  7. FeatureDescription,
  8. IntegrationFeatures,
  9. IntegrationInstallation,
  10. IntegrationMetadata,
  11. IntegrationProvider,
  12. )
  13. from sentry.integrations.mixins import RepositoryMixin
  14. from sentry.integrations.utils import AtlassianConnectValidationError, get_integration_from_request
  15. from sentry.models import Repository
  16. from sentry.pipeline import NestedPipelineView, PipelineView
  17. from sentry.shared_integrations.exceptions import ApiError
  18. from sentry.tasks.integrations import migrate_repo
  19. from sentry.utils.http import absolute_uri
  20. from .client import BitbucketApiClient
  21. from .issues import BitbucketIssueBasicMixin
  22. from .repository import BitbucketRepositoryProvider
  23. DESCRIPTION = """
  24. Connect your Sentry organization to Bitbucket, enabling the following features:
  25. """
  26. FEATURES = [
  27. FeatureDescription(
  28. """
  29. Track commits and releases (learn more
  30. [here](https://docs.sentry.io/learn/releases/))
  31. """,
  32. IntegrationFeatures.COMMITS,
  33. ),
  34. FeatureDescription(
  35. """
  36. Resolve Sentry issues via Bitbucket commits by
  37. including `Fixes PROJ-ID` in the message
  38. """,
  39. IntegrationFeatures.COMMITS,
  40. ),
  41. FeatureDescription(
  42. """
  43. Create Bitbucket issues from Sentry
  44. """,
  45. IntegrationFeatures.ISSUE_BASIC,
  46. ),
  47. FeatureDescription(
  48. """
  49. Link Sentry issues to existing Bitbucket issues
  50. """,
  51. IntegrationFeatures.ISSUE_BASIC,
  52. ),
  53. ]
  54. metadata = IntegrationMetadata(
  55. description=DESCRIPTION.strip(),
  56. features=FEATURES,
  57. author="The Sentry Team",
  58. noun=_("Installation"),
  59. issue_url="https://github.com/getsentry/sentry/issues/new?assignees=&labels=Component:%20Integrations&template=bug.yml&title=Bitbucket%20Integration%20Problem",
  60. source_url="https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/bitbucket",
  61. aspects={},
  62. )
  63. # see https://developer.atlassian.com/bitbucket/api/2/reference/meta/authentication#scopes-bbc
  64. scopes = ("issue:write", "pullrequest", "webhook", "repository")
  65. class BitbucketIntegration(IntegrationInstallation, BitbucketIssueBasicMixin, RepositoryMixin):
  66. repo_search = True
  67. def get_client(self):
  68. return BitbucketApiClient(
  69. self.model.metadata["base_url"],
  70. self.model.metadata["shared_secret"],
  71. self.model.external_id,
  72. )
  73. @property
  74. def username(self):
  75. return self.model.name
  76. def error_message_from_json(self, data):
  77. return data.get("error", {}).get("message", "unknown error")
  78. def get_repositories(self, query=None):
  79. username = self.model.metadata.get("uuid", self.username)
  80. if not query:
  81. resp = self.get_client().get_repos(username)
  82. return [
  83. {"identifier": repo["full_name"], "name": repo["full_name"]}
  84. for repo in resp.get("values", [])
  85. ]
  86. exact_query = f'name="{query}"'.encode()
  87. fuzzy_query = f'name~"{query}"'.encode()
  88. exact_search_resp = self.get_client().search_repositories(username, exact_query)
  89. fuzzy_search_resp = self.get_client().search_repositories(username, fuzzy_query)
  90. result = OrderedSet()
  91. for j in exact_search_resp.get("values", []):
  92. result.add(j["full_name"])
  93. for i in fuzzy_search_resp.get("values", []):
  94. result.add(i["full_name"])
  95. return [{"identifier": full_name, "name": full_name} for full_name in result]
  96. def has_repo_access(self, repo):
  97. client = self.get_client()
  98. try:
  99. client.get_hooks(repo.config["name"])
  100. except ApiError:
  101. return False
  102. return True
  103. def get_unmigratable_repositories(self):
  104. repos = Repository.objects.filter(
  105. organization_id=self.organization_id, provider="bitbucket"
  106. )
  107. accessible_repos = [r["identifier"] for r in self.get_repositories()]
  108. return [repo for repo in repos if repo.name not in accessible_repos]
  109. def reinstall(self):
  110. self.reinstall_repositories()
  111. class BitbucketIntegrationProvider(IntegrationProvider):
  112. key = "bitbucket"
  113. name = "Bitbucket"
  114. metadata = metadata
  115. scopes = scopes
  116. integration_cls = BitbucketIntegration
  117. features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.COMMITS])
  118. def get_pipeline_views(self):
  119. identity_pipeline_config = {"redirect_url": absolute_uri("/extensions/bitbucket/setup/")}
  120. identity_pipeline_view = NestedPipelineView(
  121. bind_key="identity",
  122. provider_key="bitbucket",
  123. pipeline_cls=IdentityProviderPipeline,
  124. config=identity_pipeline_config,
  125. )
  126. return [identity_pipeline_view, VerifyInstallation()]
  127. def post_install(self, integration, organization, extra=None):
  128. repo_ids = Repository.objects.filter(
  129. organization_id=organization.id,
  130. provider__in=["bitbucket", "integrations:bitbucket"],
  131. integration_id__isnull=True,
  132. ).values_list("id", flat=True)
  133. for repo_id in repo_ids:
  134. migrate_repo.apply_async(
  135. kwargs={
  136. "repo_id": repo_id,
  137. "integration_id": integration.id,
  138. "organization_id": organization.id,
  139. }
  140. )
  141. def build_integration(self, state):
  142. if state.get("publicKey"):
  143. principal_data = state["principal"]
  144. base_url = state["baseUrl"].replace("https://", "")
  145. # fall back to display name, user installations will use this primarily
  146. username = principal_data.get("username", principal_data["display_name"])
  147. account_type = principal_data["type"]
  148. domain = f"{base_url}/{username}" if account_type == "team" else username
  149. return {
  150. "provider": self.key,
  151. "external_id": state["clientKey"],
  152. "name": username,
  153. "metadata": {
  154. "public_key": state["publicKey"],
  155. "shared_secret": state["sharedSecret"],
  156. "base_url": state["baseApiUrl"],
  157. "domain_name": domain,
  158. "icon": principal_data["links"]["avatar"]["href"],
  159. "scopes": self.scopes,
  160. "uuid": principal_data["uuid"],
  161. "type": account_type, # team or user account
  162. },
  163. }
  164. else:
  165. return {
  166. "provider": self.key,
  167. "external_id": state["external_id"],
  168. "expect_exists": True,
  169. }
  170. def setup(self):
  171. from sentry.plugins.base import bindings
  172. bindings.add(
  173. "integration-repository.provider",
  174. BitbucketRepositoryProvider,
  175. id=f"integrations:{self.key}",
  176. )
  177. class VerifyInstallation(PipelineView):
  178. def dispatch(self, request: Request, pipeline) -> Response:
  179. try:
  180. integration = get_integration_from_request(request, BitbucketIntegrationProvider.key)
  181. except AtlassianConnectValidationError:
  182. return pipeline.error("Unable to verify installation.")
  183. pipeline.bind_state("external_id", integration.external_id)
  184. return pipeline.next_step()