We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Create comment when linking to existing Jira issue
## Summary

It would be nice if when linking to an existing Jira issue Sentry created a comment with a link back to the Sentry issue.

## Motivation

- Sentry already creates a backlink when creating a _new_ Jira issue
- Being able to navigate from Jira issue to the original Sentry issue is just as important and useful as navigating from Sentry to Jira.

## Additional Context

When creating a new Jira issue, there is a backlink:

![image](https://user-images.githubusercontent.com/52021/98541556-ae574a00-22a0-11eb-9711-4c7f1ef3dbc6.png)

![image](https://user-images.githubusercontent.com/52021/98541575-b31bfe00-22a0-11eb-89dc-0c057ee1519b.png)

![image](https://user-images.githubusercontent.com/52021/98541590-b911df00-22a0-11eb-902f-49981ccbf1bb.png)

When linking to an existing Jira issue, there is no comment and no backlink.

We are using on-premises Jira Server.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/integrations/jira_server/integration.py`
Content:
```
1 import logging
2 
3 from cryptography.hazmat.primitives.serialization import load_pem_private_key
4 from cryptography.hazmat.backends import default_backend
5 from django import forms
6 from django.core.urlresolvers import reverse
7 from django.core.validators import URLValidator
8 from django.utils.translation import ugettext as _
9 from django.views.decorators.csrf import csrf_exempt
10 from urllib.parse import urlparse
11 
12 from sentry.integrations import (
13     IntegrationFeatures,
14     IntegrationProvider,
15     IntegrationMetadata,
16     FeatureDescription,
17 )
18 from sentry.shared_integrations.exceptions import IntegrationError, ApiError
19 from sentry.integrations.jira import JiraIntegration
20 from sentry.pipeline import PipelineView
21 from sentry.utils.hashlib import sha1_text
22 from sentry.utils.decorators import classproperty
23 from sentry.web.helpers import render_to_response
24 from .client import JiraServer, JiraServerSetupClient, JiraServerClient
25 
26 
27 logger = logging.getLogger("sentry.integrations.jira_server")
28 
29 DESCRIPTION = """
30 Connect your Sentry organization into one or more of your Jira Server instances.
31 Get started streamlining your bug squashing workflow by unifying your Sentry and
32 Jira instances together.
33 """
34 
35 FEATURE_DESCRIPTIONS = [
36     FeatureDescription(
37         """
38         Create and link Sentry issue groups directly to a Jira ticket in any of your
39         projects, providing a quick way to jump from Sentry bug to tracked ticket!
40         """,
41         IntegrationFeatures.ISSUE_BASIC,
42     ),
43     FeatureDescription(
44         """
45         Automatically synchronize assignees to and from Jira. Don't get confused
46         who's fixing what, let us handle ensuring your issues and tickets match up
47         to your Sentry and Jira assignees.
48         """,
49         IntegrationFeatures.ISSUE_SYNC,
50     ),
51     FeatureDescription(
52         """
53         Synchronize Comments on Sentry Issues directly to the linked Jira ticket.
54         """,
55         IntegrationFeatures.ISSUE_SYNC,
56     ),
57 ]
58 
59 setup_alert = {
60     "type": "warning",
61     "icon": "icon-warning-sm",
62     "text": "Your Jira instance must be able to communicate with Sentry."
63     " Sentry makes outbound requests from a [static set of IP"
64     " addresses](https://docs.sentry.io/ip-ranges/) that you may wish"
65     " to allow in your firewall to support this integration.",
66 }
67 
68 
69 metadata = IntegrationMetadata(
70     description=_(DESCRIPTION.strip()),
71     features=FEATURE_DESCRIPTIONS,
72     author="The Sentry Team",
73     noun=_("Installation"),
74     issue_url="https://github.com/getsentry/sentry/issues/new?assignees=&labels=Component:%20Integrations&template=bug_report.md&title=Jira%20Server%20Integration%20Problem",
75     source_url="https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/jira_server",
76     aspects={"alerts": [setup_alert]},
77 )
78 
79 
80 class InstallationForm(forms.Form):
81     url = forms.CharField(
82         label=_("Jira URL"),
83         help_text=_("The base URL for your Jira Server instance, including the host and protocol."),
84         widget=forms.TextInput(attrs={"placeholder": "https://jira.example.com"}),
85         validators=[URLValidator()],
86     )
87     verify_ssl = forms.BooleanField(
88         label=_("Verify SSL"),
89         help_text=_(
90             "By default, we verify SSL certificates " "when making requests to your Jira instance."
91         ),
92         widget=forms.CheckboxInput(),
93         required=False,
94         initial=True,
95     )
96     consumer_key = forms.CharField(
97         label=_("Jira Consumer Key"),
98         widget=forms.TextInput(attrs={"placeholder": "sentry-consumer-key"}),
99     )
100     private_key = forms.CharField(
101         label=_("Jira Consumer Private Key"),
102         widget=forms.Textarea(
103             attrs={
104                 "placeholder": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
105             }
106         ),
107     )
108 
109     def clean_url(self):
110         """Strip off trailing / as they cause invalid URLs downstream"""
111         return self.cleaned_data["url"].rstrip("/")
112 
113     def clean_private_key(self):
114         data = self.cleaned_data["private_key"]
115 
116         try:
117             load_pem_private_key(data.encode("utf-8"), None, default_backend())
118         except Exception:
119             raise forms.ValidationError(
120                 "Private key must be a valid SSH private key encoded in a PEM format."
121             )
122         return data
123 
124     def clean_consumer_key(self):
125         data = self.cleaned_data["consumer_key"]
126         if len(data) > 200:
127             raise forms.ValidationError("Consumer key is limited to 200 characters.")
128         return data
129 
130 
131 class InstallationConfigView(PipelineView):
132     """
133     Collect the OAuth client credentials from the user.
134     """
135 
136     def dispatch(self, request, pipeline):
137         if request.method == "POST":
138             form = InstallationForm(request.POST)
139             if form.is_valid():
140                 form_data = form.cleaned_data
141 
142                 pipeline.bind_state("installation_data", form_data)
143                 return pipeline.next_step()
144         else:
145             form = InstallationForm()
146 
147         return render_to_response(
148             template="sentry/integrations/jira-server-config.html",
149             context={"form": form},
150             request=request,
151         )
152 
153 
154 class OAuthLoginView(PipelineView):
155     """
156     Start the OAuth dance by creating a request token
157     and redirecting the user to approve it.
158     """
159 
160     @csrf_exempt
161     def dispatch(self, request, pipeline):
162         if "oauth_token" in request.GET:
163             return pipeline.next_step()
164 
165         config = pipeline.fetch_state("installation_data")
166         client = JiraServerSetupClient(
167             config.get("url"),
168             config.get("consumer_key"),
169             config.get("private_key"),
170             config.get("verify_ssl"),
171         )
172         try:
173             request_token = client.get_request_token()
174             pipeline.bind_state("request_token", request_token)
175             authorize_url = client.get_authorize_url(request_token)
176 
177             return self.redirect(authorize_url)
178         except ApiError as error:
179             logger.info(
180                 "identity.jira-server.request-token",
181                 extra={"url": config.get("url"), "error": error},
182             )
183             return pipeline.error("Could not fetch a request token from Jira. %s" % error)
184 
185 
186 class OAuthCallbackView(PipelineView):
187     """
188     Complete the OAuth dance by exchanging our request token
189     into an access token.
190     """
191 
192     @csrf_exempt
193     def dispatch(self, request, pipeline):
194         config = pipeline.fetch_state("installation_data")
195         client = JiraServerSetupClient(
196             config.get("url"),
197             config.get("consumer_key"),
198             config.get("private_key"),
199             config.get("verify_ssl"),
200         )
201 
202         try:
203             access_token = client.get_access_token(
204                 pipeline.fetch_state("request_token"), request.GET["oauth_token"]
205             )
206             pipeline.bind_state("access_token", access_token)
207 
208             return pipeline.next_step()
209         except ApiError as error:
210             logger.info("identity.jira-server.access-token", extra={"error": error})
211             return pipeline.error("Could not fetch an access token from Jira")
212 
213 
214 class JiraServerIntegration(JiraIntegration):
215     """
216     IntegrationInstallation implementation for Jira-Server
217     """
218 
219     default_identity = None
220 
221     @classproperty
222     def use_email_scope(cls):
223         # jira server doesn't need the email scope since it's not restricted by GDPR
224         return False
225 
226     def get_client(self):
227         if self.default_identity is None:
228             self.default_identity = self.get_default_identity()
229 
230         return JiraServerClient(
231             self.model.metadata["base_url"],
232             JiraServer(self.default_identity.data),
233             self.model.metadata["verify_ssl"],
234         )
235 
236     def get_link_issue_config(self, group, **kwargs):
237         fields = super(JiraIntegration, self).get_link_issue_config(group, **kwargs)
238         org = group.organization
239         autocomplete_url = reverse(
240             "sentry-extensions-jiraserver-search", args=[org.slug, self.model.id]
241         )
242         for field in fields:
243             if field["name"] == "externalIssue":
244                 field["url"] = autocomplete_url
245                 field["type"] = "select"
246         return fields
247 
248     def search_url(self, org_slug):
249         return reverse("sentry-extensions-jiraserver-search", args=[org_slug, self.model.id])
250 
251 
252 class JiraServerIntegrationProvider(IntegrationProvider):
253     key = "jira_server"
254     name = "Jira Server"
255     metadata = metadata
256     integration_cls = JiraServerIntegration
257 
258     needs_default_identity = True
259 
260     can_add = True
261 
262     features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.ISSUE_SYNC])
263 
264     setup_dialog_config = {"width": 1030, "height": 1000}
265 
266     def get_pipeline_views(self):
267         return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()]
268 
269     def build_integration(self, state):
270         install = state["installation_data"]
271         access_token = state["access_token"]
272 
273         webhook_secret = sha1_text(install["private_key"]).hexdigest()
274 
275         hostname = urlparse(install["url"]).netloc
276         external_id = "{}:{}".format(hostname, install["consumer_key"])[:64]
277 
278         credentials = {
279             "consumer_key": install["consumer_key"],
280             "private_key": install["private_key"],
281             "access_token": access_token["oauth_token"],
282             "access_token_secret": access_token["oauth_token_secret"],
283         }
284         # Create the webhook before the integration record exists
285         # so that if it fails we don't persist a broken integration.
286         self.create_webhook(external_id, webhook_secret, install, credentials)
287 
288         return {
289             "name": install["consumer_key"],
290             "provider": "jira_server",
291             "external_id": external_id,
292             "metadata": {
293                 "base_url": install["url"],
294                 "domain_name": hostname,
295                 "verify_ssl": install["verify_ssl"],
296                 "webhook_secret": webhook_secret,
297             },
298             "user_identity": {
299                 "type": "jira_server",
300                 "external_id": external_id,
301                 "scopes": [],
302                 "data": credentials,
303             },
304         }
305 
306     def create_webhook(self, external_id, webhook_secret, install, credentials):
307         client = JiraServerSetupClient(
308             install["url"], install["consumer_key"], install["private_key"], install["verify_ssl"]
309         )
310         try:
311             client.create_issue_webhook(external_id, webhook_secret, credentials)
312         except ApiError as err:
313             logger.info(
314                 "jira-server.webhook.failed",
315                 extra={"error": str(err), "external_id": external_id},
316             )
317             try:
318                 details = next(x for x in err.json["messages"][0].values())
319             except (KeyError, TypeError, StopIteration):
320                 details = ""
321             message = f"Could not create issue webhook in Jira. {details}"
322             raise IntegrationError(message)
323
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/sentry/integrations/jira_server/integration.py b/src/sentry/integrations/jira_server/integration.py
--- a/src/sentry/integrations/jira_server/integration.py
+++ b/src/sentry/integrations/jira_server/integration.py
@@ -18,8 +18,9 @@
 from sentry.shared_integrations.exceptions import IntegrationError, ApiError
 from sentry.integrations.jira import JiraIntegration
 from sentry.pipeline import PipelineView
-from sentry.utils.hashlib import sha1_text
 from sentry.utils.decorators import classproperty
+from sentry.utils.hashlib import sha1_text
+from sentry.utils.http import absolute_uri
 from sentry.web.helpers import render_to_response
 from .client import JiraServer, JiraServerSetupClient, JiraServerClient
 
@@ -234,7 +235,8 @@
         )
 
     def get_link_issue_config(self, group, **kwargs):
-        fields = super(JiraIntegration, self).get_link_issue_config(group, **kwargs)
+        fields = super().get_link_issue_config(group, **kwargs)
+
         org = group.organization
         autocomplete_url = reverse(
             "sentry-extensions-jiraserver-search", args=[org.slug, self.model.id]
@@ -243,11 +245,35 @@
             if field["name"] == "externalIssue":
                 field["url"] = autocomplete_url
                 field["type"] = "select"
+
+        default_comment = "Linked Sentry Issue: [{}|{}]".format(
+            group.qualified_short_id,
+            absolute_uri(group.get_absolute_url(params={"referrer": "jira_server"})),
+        )
+        fields.append(
+            {
+                "name": "comment",
+                "label": "Comment",
+                "default": default_comment,
+                "type": "textarea",
+                "autosize": True,
+                "maxRows": 10,
+            }
+        )
+
         return fields
 
     def search_url(self, org_slug):
         return reverse("sentry-extensions-jiraserver-search", args=[org_slug, self.model.id])
 
+    def after_link_issue(self, external_issue, data=None, **kwargs):
+        super().after_link_issue(external_issue, **kwargs)
+
+        if data:
+            comment = data.get("comment")
+            if comment:
+                self.get_client().create_comment(external_issue.key, comment)
+
 
 class JiraServerIntegrationProvider(IntegrationProvider):
     key = "jira_server"

{"golden_diff": "diff --git a/src/sentry/integrations/jira_server/integration.py b/src/sentry/integrations/jira_server/integration.py\n--- a/src/sentry/integrations/jira_server/integration.py\n+++ b/src/sentry/integrations/jira_server/integration.py\n@@ -18,8 +18,9 @@\n from sentry.shared_integrations.exceptions import IntegrationError, ApiError\n from sentry.integrations.jira import JiraIntegration\n from sentry.pipeline import PipelineView\n-from sentry.utils.hashlib import sha1_text\n from sentry.utils.decorators import classproperty\n+from sentry.utils.hashlib import sha1_text\n+from sentry.utils.http import absolute_uri\n from sentry.web.helpers import render_to_response\n from .client import JiraServer, JiraServerSetupClient, JiraServerClient\n \n@@ -234,7 +235,8 @@\n         )\n \n     def get_link_issue_config(self, group, **kwargs):\n-        fields = super(JiraIntegration, self).get_link_issue_config(group, **kwargs)\n+        fields = super().get_link_issue_config(group, **kwargs)\n+\n         org = group.organization\n         autocomplete_url = reverse(\n             \"sentry-extensions-jiraserver-search\", args=[org.slug, self.model.id]\n@@ -243,11 +245,35 @@\n             if field[\"name\"] == \"externalIssue\":\n                 field[\"url\"] = autocomplete_url\n                 field[\"type\"] = \"select\"\n+\n+        default_comment = \"Linked Sentry Issue: [{}|{}]\".format(\n+            group.qualified_short_id,\n+            absolute_uri(group.get_absolute_url(params={\"referrer\": \"jira_server\"})),\n+        )\n+        fields.append(\n+            {\n+                \"name\": \"comment\",\n+                \"label\": \"Comment\",\n+                \"default\": default_comment,\n+                \"type\": \"textarea\",\n+                \"autosize\": True,\n+                \"maxRows\": 10,\n+            }\n+        )\n+\n         return fields\n \n     def search_url(self, org_slug):\n         return reverse(\"sentry-extensions-jiraserver-search\", args=[org_slug, self.model.id])\n \n+    def after_link_issue(self, external_issue, data=None, **kwargs):\n+        super().after_link_issue(external_issue, **kwargs)\n+\n+        if data:\n+            comment = data.get(\"comment\")\n+            if comment:\n+                self.get_client().create_comment(external_issue.key, comment)\n+\n \n class JiraServerIntegrationProvider(IntegrationProvider):\n     key = \"jira_server\"\n", "issue": "Create comment when linking to existing Jira issue\n## Summary\r\n\r\nIt would be nice if when linking to an existing Jira issue Sentry created a comment with a link back to the Sentry issue.\r\n\r\n## Motivation\r\n\r\n- Sentry already creates a backlink when creating a _new_ Jira issue\r\n- Being able to navigate from Jira issue to the original Sentry issue is just as important and useful as navigating from Sentry to Jira.\r\n\r\n## Additional Context\r\n\r\nWhen creating a new Jira issue, there is a backlink:\r\n\r\n![image](https://user-images.githubusercontent.com/52021/98541556-ae574a00-22a0-11eb-9711-4c7f1ef3dbc6.png)\r\n\r\n![image](https://user-images.githubusercontent.com/52021/98541575-b31bfe00-22a0-11eb-89dc-0c057ee1519b.png)\r\n\r\n![image](https://user-images.githubusercontent.com/52021/98541590-b911df00-22a0-11eb-902f-49981ccbf1bb.png)\r\n\r\nWhen linking to an existing Jira issue, there is no comment and no backlink.\r\n\r\nWe are using on-premises Jira Server.\n", "before_files": [{"content": "import logging\n\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nfrom cryptography.hazmat.backends import default_backend\nfrom django import forms\nfrom django.core.urlresolvers import reverse\nfrom django.core.validators import URLValidator\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.csrf import csrf_exempt\nfrom urllib.parse import urlparse\n\nfrom sentry.integrations import (\n    IntegrationFeatures,\n    IntegrationProvider,\n    IntegrationMetadata,\n    FeatureDescription,\n)\nfrom sentry.shared_integrations.exceptions import IntegrationError, ApiError\nfrom sentry.integrations.jira import JiraIntegration\nfrom sentry.pipeline import PipelineView\nfrom sentry.utils.hashlib import sha1_text\nfrom sentry.utils.decorators import classproperty\nfrom sentry.web.helpers import render_to_response\nfrom .client import JiraServer, JiraServerSetupClient, JiraServerClient\n\n\nlogger = logging.getLogger(\"sentry.integrations.jira_server\")\n\nDESCRIPTION = \"\"\"\nConnect your Sentry organization into one or more of your Jira Server instances.\nGet started streamlining your bug squashing workflow by unifying your Sentry and\nJira instances together.\n\"\"\"\n\nFEATURE_DESCRIPTIONS = [\n    FeatureDescription(\n        \"\"\"\n        Create and link Sentry issue groups directly to a Jira ticket in any of your\n        projects, providing a quick way to jump from Sentry bug to tracked ticket!\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Automatically synchronize assignees to and from Jira. Don't get confused\n        who's fixing what, let us handle ensuring your issues and tickets match up\n        to your Sentry and Jira assignees.\n        \"\"\",\n        IntegrationFeatures.ISSUE_SYNC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Synchronize Comments on Sentry Issues directly to the linked Jira ticket.\n        \"\"\",\n        IntegrationFeatures.ISSUE_SYNC,\n    ),\n]\n\nsetup_alert = {\n    \"type\": \"warning\",\n    \"icon\": \"icon-warning-sm\",\n    \"text\": \"Your Jira instance must be able to communicate with Sentry.\"\n    \" Sentry makes outbound requests from a [static set of IP\"\n    \" addresses](https://docs.sentry.io/ip-ranges/) that you may wish\"\n    \" to allow in your firewall to support this integration.\",\n}\n\n\nmetadata = IntegrationMetadata(\n    description=_(DESCRIPTION.strip()),\n    features=FEATURE_DESCRIPTIONS,\n    author=\"The Sentry Team\",\n    noun=_(\"Installation\"),\n    issue_url=\"https://github.com/getsentry/sentry/issues/new?assignees=&labels=Component:%20Integrations&template=bug_report.md&title=Jira%20Server%20Integration%20Problem\",\n    source_url=\"https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/jira_server\",\n    aspects={\"alerts\": [setup_alert]},\n)\n\n\nclass InstallationForm(forms.Form):\n    url = forms.CharField(\n        label=_(\"Jira URL\"),\n        help_text=_(\"The base URL for your Jira Server instance, including the host and protocol.\"),\n        widget=forms.TextInput(attrs={\"placeholder\": \"https://jira.example.com\"}),\n        validators=[URLValidator()],\n    )\n    verify_ssl = forms.BooleanField(\n        label=_(\"Verify SSL\"),\n        help_text=_(\n            \"By default, we verify SSL certificates \" \"when making requests to your Jira instance.\"\n        ),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=True,\n    )\n    consumer_key = forms.CharField(\n        label=_(\"Jira Consumer Key\"),\n        widget=forms.TextInput(attrs={\"placeholder\": \"sentry-consumer-key\"}),\n    )\n    private_key = forms.CharField(\n        label=_(\"Jira Consumer Private Key\"),\n        widget=forms.Textarea(\n            attrs={\n                \"placeholder\": \"-----BEGIN RSA PRIVATE KEY-----\\n...\\n-----END RSA PRIVATE KEY-----\"\n            }\n        ),\n    )\n\n    def clean_url(self):\n        \"\"\"Strip off trailing / as they cause invalid URLs downstream\"\"\"\n        return self.cleaned_data[\"url\"].rstrip(\"/\")\n\n    def clean_private_key(self):\n        data = self.cleaned_data[\"private_key\"]\n\n        try:\n            load_pem_private_key(data.encode(\"utf-8\"), None, default_backend())\n        except Exception:\n            raise forms.ValidationError(\n                \"Private key must be a valid SSH private key encoded in a PEM format.\"\n            )\n        return data\n\n    def clean_consumer_key(self):\n        data = self.cleaned_data[\"consumer_key\"]\n        if len(data) > 200:\n            raise forms.ValidationError(\"Consumer key is limited to 200 characters.\")\n        return data\n\n\nclass InstallationConfigView(PipelineView):\n    \"\"\"\n    Collect the OAuth client credentials from the user.\n    \"\"\"\n\n    def dispatch(self, request, pipeline):\n        if request.method == \"POST\":\n            form = InstallationForm(request.POST)\n            if form.is_valid():\n                form_data = form.cleaned_data\n\n                pipeline.bind_state(\"installation_data\", form_data)\n                return pipeline.next_step()\n        else:\n            form = InstallationForm()\n\n        return render_to_response(\n            template=\"sentry/integrations/jira-server-config.html\",\n            context={\"form\": form},\n            request=request,\n        )\n\n\nclass OAuthLoginView(PipelineView):\n    \"\"\"\n    Start the OAuth dance by creating a request token\n    and redirecting the user to approve it.\n    \"\"\"\n\n    @csrf_exempt\n    def dispatch(self, request, pipeline):\n        if \"oauth_token\" in request.GET:\n            return pipeline.next_step()\n\n        config = pipeline.fetch_state(\"installation_data\")\n        client = JiraServerSetupClient(\n            config.get(\"url\"),\n            config.get(\"consumer_key\"),\n            config.get(\"private_key\"),\n            config.get(\"verify_ssl\"),\n        )\n        try:\n            request_token = client.get_request_token()\n            pipeline.bind_state(\"request_token\", request_token)\n            authorize_url = client.get_authorize_url(request_token)\n\n            return self.redirect(authorize_url)\n        except ApiError as error:\n            logger.info(\n                \"identity.jira-server.request-token\",\n                extra={\"url\": config.get(\"url\"), \"error\": error},\n            )\n            return pipeline.error(\"Could not fetch a request token from Jira. %s\" % error)\n\n\nclass OAuthCallbackView(PipelineView):\n    \"\"\"\n    Complete the OAuth dance by exchanging our request token\n    into an access token.\n    \"\"\"\n\n    @csrf_exempt\n    def dispatch(self, request, pipeline):\n        config = pipeline.fetch_state(\"installation_data\")\n        client = JiraServerSetupClient(\n            config.get(\"url\"),\n            config.get(\"consumer_key\"),\n            config.get(\"private_key\"),\n            config.get(\"verify_ssl\"),\n        )\n\n        try:\n            access_token = client.get_access_token(\n                pipeline.fetch_state(\"request_token\"), request.GET[\"oauth_token\"]\n            )\n            pipeline.bind_state(\"access_token\", access_token)\n\n            return pipeline.next_step()\n        except ApiError as error:\n            logger.info(\"identity.jira-server.access-token\", extra={\"error\": error})\n            return pipeline.error(\"Could not fetch an access token from Jira\")\n\n\nclass JiraServerIntegration(JiraIntegration):\n    \"\"\"\n    IntegrationInstallation implementation for Jira-Server\n    \"\"\"\n\n    default_identity = None\n\n    @classproperty\n    def use_email_scope(cls):\n        # jira server doesn't need the email scope since it's not restricted by GDPR\n        return False\n\n    def get_client(self):\n        if self.default_identity is None:\n            self.default_identity = self.get_default_identity()\n\n        return JiraServerClient(\n            self.model.metadata[\"base_url\"],\n            JiraServer(self.default_identity.data),\n            self.model.metadata[\"verify_ssl\"],\n        )\n\n    def get_link_issue_config(self, group, **kwargs):\n        fields = super(JiraIntegration, self).get_link_issue_config(group, **kwargs)\n        org = group.organization\n        autocomplete_url = reverse(\n            \"sentry-extensions-jiraserver-search\", args=[org.slug, self.model.id]\n        )\n        for field in fields:\n            if field[\"name\"] == \"externalIssue\":\n                field[\"url\"] = autocomplete_url\n                field[\"type\"] = \"select\"\n        return fields\n\n    def search_url(self, org_slug):\n        return reverse(\"sentry-extensions-jiraserver-search\", args=[org_slug, self.model.id])\n\n\nclass JiraServerIntegrationProvider(IntegrationProvider):\n    key = \"jira_server\"\n    name = \"Jira Server\"\n    metadata = metadata\n    integration_cls = JiraServerIntegration\n\n    needs_default_identity = True\n\n    can_add = True\n\n    features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.ISSUE_SYNC])\n\n    setup_dialog_config = {\"width\": 1030, \"height\": 1000}\n\n    def get_pipeline_views(self):\n        return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()]\n\n    def build_integration(self, state):\n        install = state[\"installation_data\"]\n        access_token = state[\"access_token\"]\n\n        webhook_secret = sha1_text(install[\"private_key\"]).hexdigest()\n\n        hostname = urlparse(install[\"url\"]).netloc\n        external_id = \"{}:{}\".format(hostname, install[\"consumer_key\"])[:64]\n\n        credentials = {\n            \"consumer_key\": install[\"consumer_key\"],\n            \"private_key\": install[\"private_key\"],\n            \"access_token\": access_token[\"oauth_token\"],\n            \"access_token_secret\": access_token[\"oauth_token_secret\"],\n        }\n        # Create the webhook before the integration record exists\n        # so that if it fails we don't persist a broken integration.\n        self.create_webhook(external_id, webhook_secret, install, credentials)\n\n        return {\n            \"name\": install[\"consumer_key\"],\n            \"provider\": \"jira_server\",\n            \"external_id\": external_id,\n            \"metadata\": {\n                \"base_url\": install[\"url\"],\n                \"domain_name\": hostname,\n                \"verify_ssl\": install[\"verify_ssl\"],\n                \"webhook_secret\": webhook_secret,\n            },\n            \"user_identity\": {\n                \"type\": \"jira_server\",\n                \"external_id\": external_id,\n                \"scopes\": [],\n                \"data\": credentials,\n            },\n        }\n\n    def create_webhook(self, external_id, webhook_secret, install, credentials):\n        client = JiraServerSetupClient(\n            install[\"url\"], install[\"consumer_key\"], install[\"private_key\"], install[\"verify_ssl\"]\n        )\n        try:\n            client.create_issue_webhook(external_id, webhook_secret, credentials)\n        except ApiError as err:\n            logger.info(\n                \"jira-server.webhook.failed\",\n                extra={\"error\": str(err), \"external_id\": external_id},\n            )\n            try:\n                details = next(x for x in err.json[\"messages\"][0].values())\n            except (KeyError, TypeError, StopIteration):\n                details = \"\"\n            message = f\"Could not create issue webhook in Jira. {details}\"\n            raise IntegrationError(message)\n", "path": "src/sentry/integrations/jira_server/integration.py"}], "after_files": [{"content": "import logging\n\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nfrom cryptography.hazmat.backends import default_backend\nfrom django import forms\nfrom django.core.urlresolvers import reverse\nfrom django.core.validators import URLValidator\nfrom django.utils.translation import ugettext as _\nfrom django.views.decorators.csrf import csrf_exempt\nfrom urllib.parse import urlparse\n\nfrom sentry.integrations import (\n    IntegrationFeatures,\n    IntegrationProvider,\n    IntegrationMetadata,\n    FeatureDescription,\n)\nfrom sentry.shared_integrations.exceptions import IntegrationError, ApiError\nfrom sentry.integrations.jira import JiraIntegration\nfrom sentry.pipeline import PipelineView\nfrom sentry.utils.decorators import classproperty\nfrom sentry.utils.hashlib import sha1_text\nfrom sentry.utils.http import absolute_uri\nfrom sentry.web.helpers import render_to_response\nfrom .client import JiraServer, JiraServerSetupClient, JiraServerClient\n\n\nlogger = logging.getLogger(\"sentry.integrations.jira_server\")\n\nDESCRIPTION = \"\"\"\nConnect your Sentry organization into one or more of your Jira Server instances.\nGet started streamlining your bug squashing workflow by unifying your Sentry and\nJira instances together.\n\"\"\"\n\nFEATURE_DESCRIPTIONS = [\n    FeatureDescription(\n        \"\"\"\n        Create and link Sentry issue groups directly to a Jira ticket in any of your\n        projects, providing a quick way to jump from Sentry bug to tracked ticket!\n        \"\"\",\n        IntegrationFeatures.ISSUE_BASIC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Automatically synchronize assignees to and from Jira. Don't get confused\n        who's fixing what, let us handle ensuring your issues and tickets match up\n        to your Sentry and Jira assignees.\n        \"\"\",\n        IntegrationFeatures.ISSUE_SYNC,\n    ),\n    FeatureDescription(\n        \"\"\"\n        Synchronize Comments on Sentry Issues directly to the linked Jira ticket.\n        \"\"\",\n        IntegrationFeatures.ISSUE_SYNC,\n    ),\n]\n\nsetup_alert = {\n    \"type\": \"warning\",\n    \"icon\": \"icon-warning-sm\",\n    \"text\": \"Your Jira instance must be able to communicate with Sentry.\"\n    \" Sentry makes outbound requests from a [static set of IP\"\n    \" addresses](https://docs.sentry.io/ip-ranges/) that you may wish\"\n    \" to allow in your firewall to support this integration.\",\n}\n\n\nmetadata = IntegrationMetadata(\n    description=_(DESCRIPTION.strip()),\n    features=FEATURE_DESCRIPTIONS,\n    author=\"The Sentry Team\",\n    noun=_(\"Installation\"),\n    issue_url=\"https://github.com/getsentry/sentry/issues/new?assignees=&labels=Component:%20Integrations&template=bug_report.md&title=Jira%20Server%20Integration%20Problem\",\n    source_url=\"https://github.com/getsentry/sentry/tree/master/src/sentry/integrations/jira_server\",\n    aspects={\"alerts\": [setup_alert]},\n)\n\n\nclass InstallationForm(forms.Form):\n    url = forms.CharField(\n        label=_(\"Jira URL\"),\n        help_text=_(\"The base URL for your Jira Server instance, including the host and protocol.\"),\n        widget=forms.TextInput(attrs={\"placeholder\": \"https://jira.example.com\"}),\n        validators=[URLValidator()],\n    )\n    verify_ssl = forms.BooleanField(\n        label=_(\"Verify SSL\"),\n        help_text=_(\n            \"By default, we verify SSL certificates \" \"when making requests to your Jira instance.\"\n        ),\n        widget=forms.CheckboxInput(),\n        required=False,\n        initial=True,\n    )\n    consumer_key = forms.CharField(\n        label=_(\"Jira Consumer Key\"),\n        widget=forms.TextInput(attrs={\"placeholder\": \"sentry-consumer-key\"}),\n    )\n    private_key = forms.CharField(\n        label=_(\"Jira Consumer Private Key\"),\n        widget=forms.Textarea(\n            attrs={\n                \"placeholder\": \"-----BEGIN RSA PRIVATE KEY-----\\n...\\n-----END RSA PRIVATE KEY-----\"\n            }\n        ),\n    )\n\n    def clean_url(self):\n        \"\"\"Strip off trailing / as they cause invalid URLs downstream\"\"\"\n        return self.cleaned_data[\"url\"].rstrip(\"/\")\n\n    def clean_private_key(self):\n        data = self.cleaned_data[\"private_key\"]\n\n        try:\n            load_pem_private_key(data.encode(\"utf-8\"), None, default_backend())\n        except Exception:\n            raise forms.ValidationError(\n                \"Private key must be a valid SSH private key encoded in a PEM format.\"\n            )\n        return data\n\n    def clean_consumer_key(self):\n        data = self.cleaned_data[\"consumer_key\"]\n        if len(data) > 200:\n            raise forms.ValidationError(\"Consumer key is limited to 200 characters.\")\n        return data\n\n\nclass InstallationConfigView(PipelineView):\n    \"\"\"\n    Collect the OAuth client credentials from the user.\n    \"\"\"\n\n    def dispatch(self, request, pipeline):\n        if request.method == \"POST\":\n            form = InstallationForm(request.POST)\n            if form.is_valid():\n                form_data = form.cleaned_data\n\n                pipeline.bind_state(\"installation_data\", form_data)\n                return pipeline.next_step()\n        else:\n            form = InstallationForm()\n\n        return render_to_response(\n            template=\"sentry/integrations/jira-server-config.html\",\n            context={\"form\": form},\n            request=request,\n        )\n\n\nclass OAuthLoginView(PipelineView):\n    \"\"\"\n    Start the OAuth dance by creating a request token\n    and redirecting the user to approve it.\n    \"\"\"\n\n    @csrf_exempt\n    def dispatch(self, request, pipeline):\n        if \"oauth_token\" in request.GET:\n            return pipeline.next_step()\n\n        config = pipeline.fetch_state(\"installation_data\")\n        client = JiraServerSetupClient(\n            config.get(\"url\"),\n            config.get(\"consumer_key\"),\n            config.get(\"private_key\"),\n            config.get(\"verify_ssl\"),\n        )\n        try:\n            request_token = client.get_request_token()\n            pipeline.bind_state(\"request_token\", request_token)\n            authorize_url = client.get_authorize_url(request_token)\n\n            return self.redirect(authorize_url)\n        except ApiError as error:\n            logger.info(\n                \"identity.jira-server.request-token\",\n                extra={\"url\": config.get(\"url\"), \"error\": error},\n            )\n            return pipeline.error(\"Could not fetch a request token from Jira. %s\" % error)\n\n\nclass OAuthCallbackView(PipelineView):\n    \"\"\"\n    Complete the OAuth dance by exchanging our request token\n    into an access token.\n    \"\"\"\n\n    @csrf_exempt\n    def dispatch(self, request, pipeline):\n        config = pipeline.fetch_state(\"installation_data\")\n        client = JiraServerSetupClient(\n            config.get(\"url\"),\n            config.get(\"consumer_key\"),\n            config.get(\"private_key\"),\n            config.get(\"verify_ssl\"),\n        )\n\n        try:\n            access_token = client.get_access_token(\n                pipeline.fetch_state(\"request_token\"), request.GET[\"oauth_token\"]\n            )\n            pipeline.bind_state(\"access_token\", access_token)\n\n            return pipeline.next_step()\n        except ApiError as error:\n            logger.info(\"identity.jira-server.access-token\", extra={\"error\": error})\n            return pipeline.error(\"Could not fetch an access token from Jira\")\n\n\nclass JiraServerIntegration(JiraIntegration):\n    \"\"\"\n    IntegrationInstallation implementation for Jira-Server\n    \"\"\"\n\n    default_identity = None\n\n    @classproperty\n    def use_email_scope(cls):\n        # jira server doesn't need the email scope since it's not restricted by GDPR\n        return False\n\n    def get_client(self):\n        if self.default_identity is None:\n            self.default_identity = self.get_default_identity()\n\n        return JiraServerClient(\n            self.model.metadata[\"base_url\"],\n            JiraServer(self.default_identity.data),\n            self.model.metadata[\"verify_ssl\"],\n        )\n\n    def get_link_issue_config(self, group, **kwargs):\n        fields = super().get_link_issue_config(group, **kwargs)\n\n        org = group.organization\n        autocomplete_url = reverse(\n            \"sentry-extensions-jiraserver-search\", args=[org.slug, self.model.id]\n        )\n        for field in fields:\n            if field[\"name\"] == \"externalIssue\":\n                field[\"url\"] = autocomplete_url\n                field[\"type\"] = \"select\"\n\n        default_comment = \"Linked Sentry Issue: [{}|{}]\".format(\n            group.qualified_short_id,\n            absolute_uri(group.get_absolute_url(params={\"referrer\": \"jira_server\"})),\n        )\n        fields.append(\n            {\n                \"name\": \"comment\",\n                \"label\": \"Comment\",\n                \"default\": default_comment,\n                \"type\": \"textarea\",\n                \"autosize\": True,\n                \"maxRows\": 10,\n            }\n        )\n\n        return fields\n\n    def search_url(self, org_slug):\n        return reverse(\"sentry-extensions-jiraserver-search\", args=[org_slug, self.model.id])\n\n    def after_link_issue(self, external_issue, data=None, **kwargs):\n        super().after_link_issue(external_issue, **kwargs)\n\n        if data:\n            comment = data.get(\"comment\")\n            if comment:\n                self.get_client().create_comment(external_issue.key, comment)\n\n\nclass JiraServerIntegrationProvider(IntegrationProvider):\n    key = \"jira_server\"\n    name = \"Jira Server\"\n    metadata = metadata\n    integration_cls = JiraServerIntegration\n\n    needs_default_identity = True\n\n    can_add = True\n\n    features = frozenset([IntegrationFeatures.ISSUE_BASIC, IntegrationFeatures.ISSUE_SYNC])\n\n    setup_dialog_config = {\"width\": 1030, \"height\": 1000}\n\n    def get_pipeline_views(self):\n        return [InstallationConfigView(), OAuthLoginView(), OAuthCallbackView()]\n\n    def build_integration(self, state):\n        install = state[\"installation_data\"]\n        access_token = state[\"access_token\"]\n\n        webhook_secret = sha1_text(install[\"private_key\"]).hexdigest()\n\n        hostname = urlparse(install[\"url\"]).netloc\n        external_id = \"{}:{}\".format(hostname, install[\"consumer_key\"])[:64]\n\n        credentials = {\n            \"consumer_key\": install[\"consumer_key\"],\n            \"private_key\": install[\"private_key\"],\n            \"access_token\": access_token[\"oauth_token\"],\n            \"access_token_secret\": access_token[\"oauth_token_secret\"],\n        }\n        # Create the webhook before the integration record exists\n        # so that if it fails we don't persist a broken integration.\n        self.create_webhook(external_id, webhook_secret, install, credentials)\n\n        return {\n            \"name\": install[\"consumer_key\"],\n            \"provider\": \"jira_server\",\n            \"external_id\": external_id,\n            \"metadata\": {\n                \"base_url\": install[\"url\"],\n                \"domain_name\": hostname,\n                \"verify_ssl\": install[\"verify_ssl\"],\n                \"webhook_secret\": webhook_secret,\n            },\n            \"user_identity\": {\n                \"type\": \"jira_server\",\n                \"external_id\": external_id,\n                \"scopes\": [],\n                \"data\": credentials,\n            },\n        }\n\n    def create_webhook(self, external_id, webhook_secret, install, credentials):\n        client = JiraServerSetupClient(\n            install[\"url\"], install[\"consumer_key\"], install[\"private_key\"], install[\"verify_ssl\"]\n        )\n        try:\n            client.create_issue_webhook(external_id, webhook_secret, credentials)\n        except ApiError as err:\n            logger.info(\n                \"jira-server.webhook.failed\",\n                extra={\"error\": str(err), \"external_id\": external_id},\n            )\n            try:\n                details = next(x for x in err.json[\"messages\"][0].values())\n            except (KeyError, TypeError, StopIteration):\n                details = \"\"\n            message = f\"Could not create issue webhook in Jira. {details}\"\n            raise IntegrationError(message)\n", "path": "src/sentry/integrations/jira_server/integration.py"}]}