def get_jira_auth_from_request(request): # https://developer.atlassian.com/static/connect/docs/latest/concepts/authentication.html # Extract the JWT token from the request's jwt query # parameter or the authorization header. token = request.GET.get("jwt") if token is None: raise ApiError("No token parameter") # Decode the JWT token, without verification. This gives # you a header JSON object, a claims JSON object, and a signature. decoded = jwt.peek_claims(token) # Extract the issuer ('iss') claim from the decoded, unverified # claims object. This is the clientKey for the tenant - an identifier # for the Atlassian application making the call issuer = decoded["iss"] # Look up the sharedSecret for the clientKey, as stored # by the add-on during the installation handshake from sentry_plugins.jira_ac.models import JiraTenant jira_auth = JiraTenant.objects.get(client_key=issuer) # Verify the signature with the sharedSecret and # the algorithm specified in the header's alg field. decoded_verified = jwt.decode(token, jira_auth.secret) # Verify the query has not been tampered by Creating a Query Hash # and comparing it against the qsh claim on the verified token. # TODO: probably shouldn't need to hardcode get... for post maybe # the secret should just be a hidden field in the form ? qsh = get_query_hash(request.path, "GET", request.GET) # qsh = get_query_hash(request.path, request.method, request.GET) if qsh != decoded_verified["qsh"]: raise ApiError("Query hash mismatch") return jira_auth
def get_create_meta_for_project(self, project): params = { "expand": "projects.issuetypes.fields", "projectIds": project } metas = self.get_cached(self.META_URL, params=params) # We saw an empty JSON response come back from the API :( if not metas: logger.info( "jira.get-create-meta.empty-response", extra={ "base_url": self.base_url, "project": project }, ) return None # XXX(dcramer): document how this is possible, if it even is if len(metas["projects"]) > 1: raise ApiError(f"More than one project found matching {project}.") try: return metas["projects"][0] except IndexError: logger.info( "jira.get-create-meta.key-error", extra={ "base_url": self.base_url, "project": project }, ) return None
def get_issue(self, repo, issue_id): try: return self.request( "GET", "/projects/{}/issues/{}".format(quote(repo, safe=""), issue_id) ) except IndexError: raise ApiError("Issue not found with ID", 404)
def test_get_message_from_error(self): self.assert_setup_flow() integration = Integration.objects.get(provider=self.provider.key) installation = integration.get_installation(self.organization) base_error = f"Error Communicating with GitHub (HTTP 404): {API_ERRORS[404]}" assert (installation.message_from_error( ApiError( "Not Found", code=404, url="https://api.github.com/repos/scefali")) == base_error) url = "https://api.github.com/repos/scefali/sentry-integration-example/compare/2adcab794f6f57efa8aa84de68a724e728395792...e208ee2d71e8426522f95efbdae8630fa66499ab" assert ( installation.message_from_error( ApiError("Not Found", code=404, url=url)) == base_error + f" Please also confirm that the commits associated with the following URL have been pushed to GitHub: {url}" )
def request(self, method, path, headers=None, data=None, params=None, json=False, timeout=None): # TODO(meredith): Slack actually supports json now for the chat.postMessage so we # can update that so we don't have to pass json=False here response = self._request(method, path, headers=headers, data=data, params=params, json=json) if not response.json.get("ok"): raise ApiError(response.get("error", "")) return response
def get_issue(self, project_id, issue_id): """Get an issue See https://docs.gitlab.com/ee/api/issues.html#single-issue """ try: return self.get(GitLabApiClientPath.issue.format(project=project_id, issue=issue_id)) except IndexError: raise ApiError("Issue not found with ID", 404)
def handle_refresh_error(self, req, payload): error_name = "unknown_error" error_description = "no description available" for name_key in ["error", "Error"]: if name_key in payload: error_name = payload.get(name_key) break for desc_key in ["error_description", "ErrorDescription"]: if desc_key in payload: error_description = payload.get(desc_key) break formatted_error = u"HTTP {} ({}): {}".format(req.status_code, error_name, error_description) if req.status_code == 401: self.logger.info( "identity.oauth.refresh.identity-not-valid-error", extra={ "error_name": error_name, "error_status_code": req.status_code, "error_description": error_description, "provider_key": self.key, }, ) raise IdentityNotValid(formatted_error) if req.status_code == 400: # this may not be common, but at the very least Google will return # an invalid grant when a user is suspended if error_name == "invalid_grant": self.logger.info( "identity.oauth.refresh.identity-not-valid-error", extra={ "error_name": error_name, "error_status_code": req.status_code, "error_description": error_description, "provider_key": self.key, }, ) raise IdentityNotValid(formatted_error) if req.status_code != 200: self.logger.info( "identity.oauth.refresh.api-error", extra={ "error_name": error_name, "error_status_code": req.status_code, "error_description": error_description, "provider_key": self.key, }, ) raise ApiError(formatted_error)
def get_create_meta_for_project(self, project): metas = self.get_create_meta(project) # We saw an empty JSON response come back from the API :( if not metas: return None # XXX(dcramer): document how this is possible, if it even is if len(metas["projects"]) > 1: raise ApiError("More than one project found.") try: return metas["projects"][0] except IndexError: return None
def request( self, method: str, path: str, headers: Optional[Mapping[str, str]] = None, data: Optional[Mapping[str, Any]] = None, params: Optional[Mapping[str, Any]] = None, json: bool = False, timeout: Optional[int] = None, ): # TODO(meredith): Slack actually supports json now for the chat.postMessage so we # can update that so we don't have to pass json=False here response = self._request(method, path, headers=headers, data=data, params=params, json=json) if not response.json.get("ok"): raise ApiError(response.get("error", "")) return response
def get_access_token(self, request_token, verifier): """ Step 3 of the oauth flow. Use the verifier and request token from step 1 to get an access token. """ if not verifier: raise ApiError("Missing OAuth token verifier") auth = OAuth1( client_key=self.consumer_key, resource_owner_key=request_token["oauth_token"], resource_owner_secret=request_token["oauth_token_secret"], verifier=verifier, rsa_key=self.private_key, signature_method=SIGNATURE_RSA, signature_type="auth_header", ) url = self.access_token_url.format(self.base_url) resp = self.post(url, auth=auth, allow_text=True) return dict(parse_qsl(resp.text))
def test_notify_failure(self): errors = ( ApiError("The server is sad"), SSLError("[SSL: UNKNOWN_PROTOCOL] unknown protocol (_ssl.c:590)"), HTTPError("A bad response"), PluginError("A plugin is sad"), ) for err in errors: n = DummyNotificationPlugin() n.slug = "slack" def hook(*a, **kw): raise err event = self.store_event(data={}, project_id=self.project.id) notification = Notification(event) n.notify_users = hook assert n.notify(notification) is False
def test_test_configuration_and_get_test_results(self): errors = ( ApiError("The server is sad"), ApiHostError("host error"), ApiUnauthorized("not used"), ) for err in errors: n = DummyNotificationPlugin() n.slug = "slack" def hook(*a, **kw): n.raise_error(err) n.notify_users = hook if isinstance(err, ApiUnauthorized): message = "your access token was invalid" else: message = err.text assert message assert message in n.test_configuration_and_get_test_results(self.project)
def test_handle_exception_503(self): resp = IntegrationEndpoint().handle_exception( HttpRequest(), ApiError("This is an error", code=503)) assert resp.status_code == 503 assert resp.exception is True
def get_user(self, user_id): user = self._get_stub_data("user.json") if user["accountId"] == user_id: return user raise ApiError("no user found")