diff --git a/src/sentry/integrations/bitbucket_server/integration.py b/src/sentry/integrations/bitbucket_server/integration.py index 807c0be881c073..76a50c95a3e899 100644 --- a/src/sentry/integrations/bitbucket_server/integration.py +++ b/src/sentry/integrations/bitbucket_server/integration.py @@ -6,14 +6,8 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.serialization import load_pem_private_key -from django import forms -from django.core.validators import URLValidator -from django.http import HttpResponseRedirect from django.http.request import HttpRequest -from django.http.response import HttpResponseBase -from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from django.views.decorators.csrf import csrf_exempt from rest_framework import serializers from rest_framework.fields import BooleanField, CharField, URLField @@ -47,7 +41,6 @@ from sentry.pipeline.views.base import ApiPipelineSteps, PipelineView from sentry.shared_integrations.exceptions import ApiError, IntegrationError from sentry.users.models.identity import Identity -from sentry.web.helpers import render_to_response from .client import BitbucketServerClient, BitbucketServerSetupClient from .repository import BitbucketServerRepositoryProvider @@ -106,161 +99,6 @@ ) -class InstallationForm(forms.Form): - url = forms.CharField( - label=_("Bitbucket URL"), - help_text=_( - "The base URL for your Bitbucket Server instance, including the host and protocol." - ), - widget=forms.TextInput(attrs={"placeholder": "https://bitbucket.example.com"}), - validators=[URLValidator()], - ) - verify_ssl = forms.BooleanField( - label=_("Verify SSL"), - help_text=_( - "By default, we verify SSL certificates " - "when making requests to your Bitbucket instance." - ), - widget=forms.CheckboxInput(), - required=False, - initial=True, - ) - consumer_key = forms.CharField( - label=_("Bitbucket Consumer Key"), - widget=forms.TextInput(attrs={"placeholder": "sentry-consumer-key"}), - ) - private_key = forms.CharField( - label=_("Bitbucket Consumer Private Key"), - widget=forms.Textarea( - attrs={ - "placeholder": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----" - } - ), - ) - - def clean_url(self): - """Strip off trailing / as they cause invalid URLs downstream""" - return self.cleaned_data["url"].rstrip("/") - - def clean_private_key(self): - data = self.cleaned_data["private_key"] - - try: - load_pem_private_key(data.encode("utf-8"), None, default_backend()) - except Exception: - raise forms.ValidationError( - "Private key must be a valid SSH private key encoded in a PEM format." - ) - return data - - def clean_consumer_key(self): - data = self.cleaned_data["consumer_key"] - if len(data) > 200: - raise forms.ValidationError("Consumer key is limited to 200 characters.") - return data - - -class InstallationConfigView: - """ - Collect the OAuth client credentials from the user. - """ - - def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase: - if request.method == "POST": - form = InstallationForm(request.POST) - if form.is_valid(): - form_data = form.cleaned_data - - pipeline.bind_state("installation_data", form_data) - return pipeline.next_step() - else: - form = InstallationForm() - - return render_to_response( - template="sentry/integrations/bitbucket-server-config.html", - context={"form": form}, - request=request, - ) - - -class OAuthLoginView: - """ - Start the OAuth dance by creating a request token - and redirecting the user to approve it. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase: - with IntegrationPipelineViewEvent( - IntegrationPipelineViewType.OAUTH_LOGIN, - IntegrationDomain.SOURCE_CODE_MANAGEMENT, - BitbucketServerIntegrationProvider.key, - ).capture() as lifecycle: - if "oauth_token" in request.GET: - return pipeline.next_step() - - config = pipeline.fetch_state("installation_data") - assert config is not None - client = BitbucketServerSetupClient( - config.get("url"), - config.get("consumer_key"), - config.get("private_key"), - config.get("verify_ssl"), - ) - - try: - request_token = client.get_request_token() - except ApiError as error: - lifecycle.record_failure(str(error), extra={"url": config.get("url")}) - return pipeline.error(f"Could not fetch a request token from Bitbucket. {error}") - - pipeline.bind_state("request_token", request_token) - if not request_token.get("oauth_token"): - lifecycle.record_failure("missing oauth_token", extra={"url": config.get("url")}) - return pipeline.error("Missing oauth_token") - - authorize_url = client.get_authorize_url(request_token) - - return HttpResponseRedirect(authorize_url) - - -class OAuthCallbackView: - """ - Complete the OAuth dance by exchanging our request token - into an access token. - """ - - @method_decorator(csrf_exempt) - def dispatch(self, request: HttpRequest, pipeline: IntegrationPipeline) -> HttpResponseBase: - with IntegrationPipelineViewEvent( - IntegrationPipelineViewType.OAUTH_CALLBACK, - IntegrationDomain.SOURCE_CODE_MANAGEMENT, - BitbucketServerIntegrationProvider.key, - ).capture() as lifecycle: - config = pipeline.fetch_state("installation_data") - assert config is not None - client = BitbucketServerSetupClient( - config.get("url"), - config.get("consumer_key"), - config.get("private_key"), - config.get("verify_ssl"), - ) - - try: - access_token = client.get_access_token( - pipeline.fetch_state("request_token"), request.GET["oauth_token"] - ) - - pipeline.bind_state("access_token", access_token) - - return pipeline.next_step() - except ApiError as error: - lifecycle.record_failure(str(error)) - return pipeline.error( - f"Could not fetch an access token from Bitbucket. {str(error)}" - ) - - class InstallationConfigData(TypedDict): url: str consumer_key: str @@ -544,7 +382,7 @@ class BitbucketServerIntegrationProvider(IntegrationProvider): setup_dialog_config = {"width": 1030, "height": 1000} def get_pipeline_views(self) -> list[PipelineView[IntegrationPipeline]]: - return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()] + return [] def get_pipeline_api_steps(self) -> ApiPipelineSteps[IntegrationPipeline]: return [InstallationConfigApiStep(), OAuthApiStep()] diff --git a/src/sentry/templates/sentry/integrations/bitbucket-server-config.html b/src/sentry/templates/sentry/integrations/bitbucket-server-config.html deleted file mode 100644 index d2e8936089f5e2..00000000000000 --- a/src/sentry/templates/sentry/integrations/bitbucket-server-config.html +++ /dev/null @@ -1,57 +0,0 @@ -{% extends "sentry/bases/modal.html" %} -{% load crispy_forms_tags %} -{% load sentry_assets %} -{% load i18n %} - -{% block css %} - -{% endblock %} - -{% block wrapperclass %} narrow auth {% endblock %} -{% block modal_header_signout %} {% endblock %} - -{% block title %} {% trans "Bitbucket-Server Setup" %} | {{ block.super }} {% endblock %} - -{% block main %} -
{% trans "Add your Bitbucket Server App credentials to Sentry." %}
-- - - {% blocktrans %} - You must complete the required steps - - in Bitbucket Server before attempting to connect with Sentry. - {% endblocktrans %} - -
- -{% endblock %} diff --git a/tests/sentry/integrations/bitbucket_server/test_integration.py b/tests/sentry/integrations/bitbucket_server/test_integration.py index 8f998c61f7e626..12ce633a5073d5 100644 --- a/tests/sentry/integrations/bitbucket_server/test_integration.py +++ b/tests/sentry/integrations/bitbucket_server/test_integration.py @@ -38,338 +38,6 @@ def integration(self): integration.add_organization(self.organization, self.user) return integration - def test_config_view(self) -> None: - resp = self.client.get(self.init_path) - assert resp.status_code == 200 - - resp = self.client.get(self.setup_path) - assert resp.status_code == 200 - self.assertContains(resp, "Connect Sentry") - self.assertContains(resp, "Submit") - - @responses.activate - def test_validate_url(self) -> None: - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 200 - self.assertContains(resp, "Enter a valid URL") - - @responses.activate - def test_validate_private_key(self) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=503, - ) - - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": "hot-garbage", - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 200 - self.assertContains( - resp, "Private key must be a valid SSH private key encoded in a PEM format." - ) - - @responses.activate - def test_validate_consumer_key_length(self) -> None: - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "x" * 201, - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 200 - self.assertContains(resp, "Consumer key is limited to 200") - - @responses.activate - @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") - def test_authentication_request_token_timeout(self, mock_record: MagicMock) -> None: - timeout = ReadTimeout("Read timed out. (read timeout=30)") - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - body=timeout, - ) - - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 200 - self.assertContains(resp, "Setup Error") - self.assertContains(resp, "request token from Bitbucket") - self.assertContains(resp, "Timed out") - - assert_failure_metric( - mock_record, "Timed out attempting to reach host: bitbucket.example.com" - ) - - @responses.activate - @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") - def test_authentication_request_token_fails(self, mock_record: MagicMock) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=503, - ) - - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 200 - self.assertContains(resp, "Setup Error") - self.assertContains(resp, "request token from Bitbucket") - - assert_failure_metric(mock_record, "") - - @responses.activate - def test_authentication_request_token_redirect(self) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=200, - content_type="text/plain", - body="oauth_token=abc123&oauth_token_secret=def456", - ) - - # Start pipeline - self.client.get(self.init_path) - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 302 - redirect = ( - "https://bitbucket.example.com/plugins/servlet/oauth/authorize?oauth_token=abc123" - ) - assert redirect == resp["Location"] - - @responses.activate - @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") - def test_authentication_access_token_failure(self, mock_record: MagicMock) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=200, - content_type="text/plain", - body="oauth_token=abc123&oauth_token_secret=def456", - ) - error_msg = "it broke" - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/access-token", - status=500, - content_type="text/plain", - body=error_msg, - ) - - # Get config page - resp = self.client.get(self.init_path) - assert resp.status_code == 200 - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 302 - assert resp["Location"] - - resp = self.client.get(self.setup_path + "?oauth_token=xyz789") - assert resp.status_code == 200 - self.assertContains(resp, "Setup Error") - self.assertContains(resp, "access token from Bitbucket") - - assert_failure_metric(mock_record, error_msg) - - def install_integration(self): - # Get config page - resp = self.client.get(self.setup_path) - assert resp.status_code == 200 - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "sentry-bot", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 302 - assert resp["Location"] - - resp = self.client.get(self.setup_path + "?oauth_token=xyz789") - assert resp.status_code == 200 - - return resp - - @responses.activate - @patch("sentry.integrations.utils.metrics.EventLifecycle.record_event") - def test_authentication_verifier_expired(self, mock_record: MagicMock) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=200, - content_type="text/plain", - body="oauth_token=abc123&oauth_token_secret=def456", - ) - error_msg = "oauth_error=token+expired" - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/access-token", - status=404, - content_type="text/plain", - body=error_msg, - ) - - # Try getting the token but it has expired for some reason, - # perhaps a stale reload/history navigate. - resp = self.install_integration() - - self.assertContains(resp, "Setup Error") - self.assertContains(resp, "access token from Bitbucket") - - assert_failure_metric(mock_record, error_msg) - - @responses.activate - def test_authentication_success(self) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=200, - content_type="text/plain", - body="oauth_token=abc123&oauth_token_secret=def456", - ) - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/access-token", - status=200, - content_type="text/plain", - body="oauth_token=valid-token&oauth_token_secret=valid-secret", - ) - responses.add( - responses.POST, - "https://bitbucket.example.com/rest/webhooks/1.0/webhook", - status=204, - body="", - ) - - self.install_integration() - - integration = Integration.objects.get() - assert integration.name == "sentry-bot" - assert integration.metadata["domain_name"] == "bitbucket.example.com" - assert integration.metadata["base_url"] == "https://bitbucket.example.com" - assert integration.metadata["verify_ssl"] is False - - org_integration = OrganizationIntegration.objects.get( - integration=integration, organization_id=self.organization.id - ) - assert org_integration.config == {} - - idp = IdentityProvider.objects.get(type="bitbucket_server") - identity = Identity.objects.get( - idp=idp, user=self.user, external_id="bitbucket.example.com:sentry-bot" - ) - assert identity.data["consumer_key"] == "sentry-bot" - assert identity.data["access_token"] == "valid-token" - assert identity.data["access_token_secret"] == "valid-secret" - assert identity.data["private_key"] == EXAMPLE_PRIVATE_KEY - - @responses.activate - def test_setup_external_id_length(self) -> None: - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/request-token", - status=200, - content_type="text/plain", - body="oauth_token=abc123&oauth_token_secret=def456", - ) - responses.add( - responses.POST, - "https://bitbucket.example.com/plugins/servlet/oauth/access-token", - status=200, - content_type="text/plain", - body="oauth_token=valid-token&oauth_token_secret=valid-secret", - ) - responses.add( - responses.POST, - "https://bitbucket.example.com/rest/webhooks/1.0/webhook", - status=204, - body="", - ) - - # Start pipeline and go to setup page. - self.client.get(self.setup_path) - - # Submit credentials - data = { - "url": "https://bitbucket.example.com/", - "verify_ssl": False, - "consumer_key": "a-very-long-consumer-key-that-when-combined-with-host-would-overflow", - "private_key": EXAMPLE_PRIVATE_KEY, - } - resp = self.client.post(self.setup_path, data=data) - assert resp.status_code == 302 - redirect = ( - "https://bitbucket.example.com/plugins/servlet/oauth/authorize?oauth_token=abc123" - ) - assert redirect == resp["Location"] - - resp = self.client.get(self.setup_path + "?oauth_token=xyz789") - assert resp.status_code == 200 - - integration = Integration.objects.get(provider="bitbucket_server") - assert ( - integration.external_id - == "bitbucket.example.com:a-very-long-consumer-key-that-when-combine" - ) - def test_source_url_matches(self) -> None: installation = self.integration.get_installation(self.organization.id)