def handle_team_member_added(self, request): data = request.data channel_data = data["channelData"] # only care if our bot is the new member added matches = filter(lambda x: x["id"] == data["recipient"]["id"], data["membersAdded"]) if not matches: return self.respond(status=204) team = channel_data["team"] # need to keep track of the service url since we won't get it later signed_data = { "team_id": team["id"], "team_name": team["name"], "service_url": data["serviceUrl"], } # sign the params so this can't be forged signed_params = sign(**signed_data) # send welcome message to the team client = get_preinstall_client(data["serviceUrl"]) card = build_welcome_card(signed_params) client.send_card(team["id"], card) return self.respond(status=201)
def handle_member_added(self, request): data = request.data channel_data = data["channelData"] # only care if our bot is the new member added matches = filter(lambda x: x["id"] == data["recipient"]["id"], data["membersAdded"]) if not matches: return self.respond(status=204) team = channel_data["team"] # TODO: add try/except for request exceptions access_token = get_token_data()["access_token"] # need to keep track of the service url since we won't get it later signed_data = { "team_id": team["id"], "team_name": team["name"], "service_url": data["serviceUrl"], "expiration_time": int(time.time()) + INSTALL_EXPIRATION_TIME, } # sign the params so this can't be forged signed_params = sign(**signed_data) # send welcome message to the team client = MsTeamsPreInstallClient(access_token, data["serviceUrl"]) card = build_welcome_card(signed_params) client.send_card(team["id"], card) return self.respond(status=201)
def post(self, request): # verify_signature will raise the exception corresponding to the error verify_signature(request) data = request.data channel_data = data["channelData"] event = channel_data.get("eventType") # TODO: Handle other events if event == "teamMemberAdded": # only care if our bot is the new member added matches = filter(lambda x: x["id"] == data["recipient"]["id"], data["membersAdded"]) if matches: team_id = channel_data["team"]["id"] access_token = get_token_data()["access_token"] # need to keep track of the service url since we won't get it later signed_data = { "team_id": team_id, "service_url": data["serviceUrl"] } # sign the params so this can't be forged signed_params = sign(**signed_data) # send welcome message to the team client = MsTeamsPreInstallClient(access_token, data["serviceUrl"]) client.send_welcome_message(team_id, signed_params) return self.respond(status=200)
def post(self, request): is_valid = verify_signature(request) if not is_valid: logger.error("msteams.webhook.invalid-signature") return self.respond(status=401) data = request.data channel_data = data["channelData"] event = channel_data.get("eventType") # TODO: Handle other events if event == "teamMemberAdded": # only care if our bot is the new member added matches = filter(lambda x: x["id"] == data["recipient"]["id"], data["membersAdded"]) if matches: # send welcome message to the team team_id = channel_data["team"]["id"] client = MsTeamsClient() # sign the params so this can't be forged signed_params = sign(team_id=team_id) url = u"%s?signed_params=%s" % ( absolute_uri("/extensions/msteams/configure/"), signed_params, ) # TODO: Better message payload = { "type": "message", "text": url, } client.send_message(team_id, payload) return self.respond(status=200)
def assert_setup_flow(self): responses.reset() responses.add( responses.POST, u"https://smba.trafficmanager.net/amer/v3/conversations/%s/activities" % team_id, json={}, ) with patch("time.time") as mock_time: mock_time.return_value = self.start_time # token mock access_json = {"expires_in": 86399, "access_token": "my_token"} responses.add( responses.POST, "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token", json=access_json, ) params = {"signed_params": sign(**self.pipeline_state)} self.pipeline.bind_state(self.provider.key, self.pipeline_state) resp = self.client.get(self.setup_path, params) body = responses.calls[0].request.body assert body == urlencode({ "client_id": "msteams-client-id", "client_secret": "msteams-client-secret", "grant_type": "client_credentials", "scope": "https://api.botframework.com/.default", }) assert resp.status_code == 200 self.assertDialogSuccess(resp) integration = Integration.objects.get(provider=self.provider.key) assert integration.external_id == team_id assert integration.name == "my_team" assert integration.metadata == { "access_token": "my_token", "service_url": "https://smba.trafficmanager.net/amer/", "expires_at": self.start_time + 86399 - 60 * 5, } assert OrganizationIntegration.objects.get( integration=integration, organization=self.organization) integration_url = u"organizations/{}/rules/".format( self.organization.slug) assert integration_url in responses.calls[1].request.body.decode( "utf-8") assert self.organization.name in responses.calls[ 1].request.body.decode("utf-8")
def test_map_params_to_state(self): config_view = JiraExtensionConfigurationView() metadata = {"my_param": "test"} data = {"metadata": json.dumps(metadata)} signed_data = sign(**data) params = {"signed_params": signed_data} assert { "metadata": metadata } == config_view.map_params_to_state(params)
def build_unlinking_url(conversation_id, service_url, teams_user_id): signed_params = sign( conversation_id=conversation_id, service_url=service_url, teams_user_id=teams_user_id, ) return absolute_uri( reverse("sentry-integration-msteams-unlink-identity", kwargs={"signed_params": signed_params}))
def test_expired(self): with patch("time.time") as mock_time: mock_time.return_value = self.start_time self.pipeline_state["expiration_time"] = self.start_time - 1 params = {"signed_params": sign(**self.pipeline_state)} self.pipeline.bind_state(self.provider.key, self.pipeline_state) resp = self.client.get(self.setup_path, params) assert resp.status_code == 200 assert "Installation link expired" in resp.content
def assert_setup_flow(self): self.login_as(self.user) signed_data = {"external_id": "my-external-id", "metadata": json.dumps(self.metadata)} params = {"signed_params": sign(**signed_data)} resp = self.client.get(self.configure_path, params) assert resp.status_code == 302 integration = Integration.objects.get(external_id="my-external-id") assert integration.metadata == self.metadata assert OrganizationIntegration.objects.filter( integration=integration, organization=self.organization ).exists()
def build_linking_url(integration, organization, slack_id, channel_id, response_url): signed_params = sign( integration_id=integration.id, organization_id=organization.id, slack_id=slack_id, channel_id=channel_id, response_url=response_url, ) return absolute_uri( reverse("sentry-integration-slack-link-identity", kwargs={"signed_params": signed_params}) )
def build_linking_url(integration, organization, teams_user_id, team_id, tenant_id): signed_params = sign( integration_id=integration.id, organization_id=organization.id, teams_user_id=teams_user_id, team_id=team_id, tenant_id=tenant_id, ) return absolute_uri( reverse("sentry-integration-msteams-link-identity", kwargs={"signed_params": signed_params}) )
def build_linking_url(integration, organization, slack_id, channel_id, response_url): signed_params = sign( integration_id=integration.id, organization_id=organization.id, slack_id=slack_id, channel_id=channel_id, response_url=response_url, ) return absolute_uri(reverse('sentry-integration-slack-link-identity', kwargs={ 'signed_params': signed_params, }))
def test_renders_template_with_signed_link(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id) resp = self.client.get(self.path + '?' + urlencode({'data': url_data})) assert resp.status_code == 200 self.assertTemplateUsed(resp, 'sentry/projects/accept_project_transfer.html') assert resp.context['project'] == self.project
def test_cannot_transfer_project_twice_from_same_org(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.post(self.path, data={"team": self.to_team.id, "data": url_data}) resp = self.client.get(self.path + "?" + urlencode({"data": url_data})) assert resp.status_code == 400
def test_handle_incorrect_url_data(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.id, # This is bad data from_organization_id=9999999, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id) resp = self.client.get(self.path + '?' + urlencode({'data': url_data})) assert resp.status_code == 302 resp = self.client.get(self.path) assert resp.status_code == 404
def build_linking_url(integration, organization, slack_id, notify_channel_id): signed_params = sign( integration_id=integration.id, organization_id=organization.id, slack_id=slack_id, notify_channel_id=notify_channel_id, ) return absolute_uri( reverse('sentry-integration-slack-link-identity', kwargs={ 'signed_params': signed_params, }))
def test_cannot_transfer_project_twice_from_same_org(self): self.login_as(self.owner) url_data = sign(actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id) url = self.path + '?' + urlencode({'data': url_data}) resp = self.client.post(url, data={'team': self.to_team.id}) assert resp['location'] == 'http://testserver' + \ reverse('sentry-organization-home', args=[self.to_team.organization.slug]) resp = self.client.get(url) assert resp.status_code == 302
def test_handle_incorrect_url_data(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.id, # This is bad data from_organization_id=9999999, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.get(self.path + "?" + urlencode({"data": url_data})) assert resp.status_code == 400 assert resp.data["detail"] == "Project no longer exists" resp = self.client.get(self.path) assert resp.status_code == 404
def test_transfers_project_to_correct_team(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.post(self.path, data={"team": self.to_team.id, "data": url_data}) assert resp.status_code == 204 p = Project.objects.get(id=self.project.id) assert p.organization_id == self.to_organization.id assert p.teams.first() == self.to_team
def test_returns_org_options_with_signed_link(self): self.login_as(self.owner) url_data = sign(actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id) resp = self.client.get(self.path + '?' + urlencode({'data': url_data})) assert resp.status_code == 200 assert resp.data['project']['slug'] == self.project.slug assert resp.data['project']['id'] == self.project.id assert len(resp.data['organizations']) == 2 org_slugs = {o['slug'] for o in resp.data['organizations']} assert self.from_organization.slug in org_slugs assert self.to_organization.slug in org_slugs
def test_transfers_project_to_correct_organization(self): self.login_as(self.owner) url_data = sign(actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id) url = self.path + '?' + urlencode({'data': url_data}) resp = self.client.post(url, data={'team': self.to_team.id}) assert resp['location'] == 'http://testserver' + \ reverse('sentry-organization-home', args=[self.to_team.organization.slug]) p = Project.objects.get(id=self.project.id) assert p.organization_id == self.to_organization.id assert p.team_id == self.to_team.id
def test_non_owner_cannot_transfer_project(self): rando_user = self.create_user(email="*****@*****.**", is_superuser=False) rando_org = self.create_organization(name="supreme beans") self.login_as(rando_user) url_data = sign( actor_id=self.member.user_id, from_organization_id=rando_org.id, project_id=self.project.id, user_id=rando_user.id, transaction_id=self.transaction_id, ) resp = self.client.post(self.path, data={"team": self.to_team.id, "data": url_data}) assert resp.status_code == 400 p = Project.objects.get(id=self.project.id) assert p.organization_id == self.from_organization.id
def test_non_owner_cannot_transfer_project(self): rando_user = self.create_user(email='*****@*****.**', is_superuser=False) rando_org = self.create_organization(name='supreme beans') self.login_as(rando_user) url_data = sign( actor_id=self.member.user_id, from_organization_id=rando_org.id, project_id=self.project.id, user_id=rando_user.id, transaction_id=self.transaction_id) url = self.path + '?' + urlencode({'data': url_data}) resp = self.client.post(url, data={'team': self.to_team.id}) assert resp.status_code == 302 p = Project.objects.get(id=self.project.id) assert p.organization_id == self.from_organization.id
def get(self, request, *args, **kwargs): try: integration = get_integration_from_request(request, "jira") except AtlassianConnectValidationError: return self.get_response( {"error_message": "Unable to verify installation."}) except ExpiredSignatureError: return self.get_response({"refresh_required": True}) # expose a link to the configuration view signed_data = { "external_id": integration.external_id, "metadata": json.dumps(integration.metadata), } finish_link = u"{}.?signed_params={}".format( absolute_uri("/extensions/jira/configure/"), sign(**signed_data)) return self.get_response({"finish_link": finish_link})
def test_returns_org_options_with_signed_link(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.get(self.path + "?" + urlencode({"data": url_data})) assert resp.status_code == 200 assert resp.data["project"]["slug"] == self.project.slug assert resp.data["project"]["id"] == self.project.id assert len(resp.data["organizations"]) == 2 org_slugs = {o["slug"] for o in resp.data["organizations"]} assert self.from_organization.slug in org_slugs assert self.to_organization.slug in org_slugs
def test_transfers_project_to_correct_organization(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.post(self.path, data={ 'organization': self.to_organization.slug, 'data': url_data }) assert resp.status_code == 204 p = Project.objects.get(id=self.project.id) assert p.organization_id == self.to_organization.id
def test_errors_when_team_and_org_provided(self): self.login_as(self.owner) url_data = sign( actor_id=self.member.user_id, from_organization_id=self.from_organization.id, project_id=self.project.id, user_id=self.owner.id, transaction_id=self.transaction_id, ) resp = self.client.post( self.path, data={ "organization": self.to_organization.slug, "team": self.to_team.id, "data": url_data, }, ) assert resp.status_code == 400 assert resp.data == {"detail": "Choose either a team or an organization, not both"} p = Project.objects.get(id=self.project.id) assert p.organization_id == self.from_organization.id
def get(self, request: Request, *args, **kwargs) -> Response: try: integration = get_integration_from_request(request, "jira") except AtlassianConnectValidationError: return self.get_response( {"error_message": UNABLE_TO_VERIFY_INSTALLATION}) except ExpiredSignatureError: return self.get_response({"refresh_required": True}) # expose a link to the configuration view signed_data = { "external_id": integration.external_id, "metadata": json.dumps(integration.metadata), } finish_link = "{}.?signed_params={}".format( absolute_uri("/extensions/jira/configure/"), sign(**signed_data)) image_path = absolute_uri( get_asset_url("sentry", "images/sentry-glyph-black.png")) return self.get_response({ "finish_link": finish_link, "image_path": image_path })
def post(self, request, project): """ Transfer a Project ```````````````` Schedules a project for transfer to a new organization. :pparam string organization_slug: the slug of the organization the project belongs to. :pparam string project_slug: the slug of the project to delete. :param string email: email of new owner. must be an organization owner :auth: required """ if project.is_internal_project(): return Response( '{"error": "Cannot transfer projects internally used by Sentry."}', status=status.HTTP_403_FORBIDDEN, ) email = request.data.get("email") if email is None: return Response(status=status.HTTP_400_BAD_REQUEST) if not request.user.is_authenticated(): return Response(status=status.HTTP_403_FORBIDDEN) try: owner = OrganizationMember.objects.filter( user__email__iexact=email, role=roles.get_top_dog().id, user__is_active=True)[0] except IndexError: return Response( { "detail": "Could not find an organization owner with that email" }, status=status.HTTP_404_NOT_FOUND, ) transaction_id = uuid4().hex url_data = sign( actor_id=request.user.id, from_organization_id=project.organization.id, project_id=project.id, user_id=owner.user_id, transaction_id=transaction_id, ) context = { "email": email, "from_org": project.organization.name, "project_name": project.slug, "request_time": timezone.now(), "url": absolute_uri("/accept-transfer/") + "?" + urlencode({"data": url_data}), "requester": request.user, } MessageBuilder( subject="{}Request for Project Transfer".format( options.get("mail.subject-prefix")), template="sentry/emails/transfer_project.txt", html_template="sentry/emails/transfer_project.html", type="org.confirm_project_transfer_request", context=context, ).send_async([email]) self.create_audit_entry( request=request, organization=project.organization, target_object=project.id, event=AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER, data=project.get_audit_log_data(), transaction_id=transaction_id, ) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, project): """ Transfer a Project ```````````````` Schedules a project for transfer to a new organization. :pparam string organization_slug: the slug of the organization the project belongs to. :pparam string project_slug: the slug of the project to delete. :param string email: email of new owner. must be an organization owner :auth: required """ if project.is_internal_project(): return Response( '{"error": "Cannot transfer projects internally used by Sentry."}', status=status.HTTP_403_FORBIDDEN ) email = request.DATA.get('email') if email is None: return Response(status=status.HTTP_400_BAD_REQUEST) if not request.user.is_authenticated(): return Response(status=status.HTTP_403_FORBIDDEN) try: owner = OrganizationMember.objects.filter( user__email__iexact=email, role=roles.get_top_dog().id, user__is_active=True, )[0] except IndexError: return Response({'detail': 'Could not find owner with that email'}, status=status.HTTP_404_NOT_FOUND) transaction_id = uuid4().hex url_data = sign( actor_id=request.user.id, from_organization_id=project.organization.id, project_id=project.id, user_id=owner.user_id, transaction_id=transaction_id) context = { 'email': email, 'from_org': project.organization.name, 'project_name': project.slug, 'request_time': timezone.now(), 'url': absolute_uri('/accept-transfer/') + '?' + urlencode({'data': url_data}), 'requester': request.user } MessageBuilder( subject='%sRequest for Project Transfer' % (options.get('mail.subject-prefix'), ), template='sentry/emails/transfer_project.txt', html_template='sentry/emails/transfer_project.html', type='org.confirm_project_transfer_request', context=context, ).send_async([email]) self.create_audit_entry( request=request, organization=project.organization, target_object=project.id, event=AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER, data=project.get_audit_log_data(), transaction_id=transaction_id, ) return Response(status=status.HTTP_204_NO_CONTENT)
def send_welcome_message(self, conversation_id): # sign the params so this can't be forged signed_params = sign(team_id=conversation_id) url = u"%s?signed_params=%s" % ( absolute_uri("/extensions/msteams/configure/"), signed_params, ) # TODO: Refactor message creation # TODO: Tweak welcome message appearance to perfection logo = { "type": "Image", "url": "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png", "size": "Medium", } welcome = { "type": "TextBlock", "weight": "Bolder", "size": "Large", "text": "Welcome to Sentry for Teams!", "wrap": True, } description = { "type": "TextBlock", "text": "The Sentry app for Teams allows you to be notified in real-time when an error pops up, using customizable alert rules.", "wrap": True, } instruction = { "type": "TextBlock", "text": "Please click [here](%s) to get started with using Sentry for Microsoft Teams." % url, "wrap": True, } card = { "type": "AdaptiveCard", "body": [ { "type": "ColumnSet", "columns": [ { "type": "Column", "items": [logo], "width": "auto" }, { "type": "Column", "items": [welcome], "width": "stretch", "verticalContentAlignment": "Center", }, ], }, description, instruction, ], "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.2", } payload = { "type": "message", "attachments": [{ "contentType": "application/vnd.microsoft.card.adaptive", "content": card }], } self.send_message(conversation_id, payload)
def handle(self, request, organization, project): form = self.get_form(request) if form.is_valid(): email = form.cleaned_data.get('email') try: owner = OrganizationMember.objects.filter( user__email__iexact=email, role=roles.get_top_dog().id, user__is_active=True, )[0] except IndexError: messages.add_message( request, messages.ERROR, six.text_type( _('Could not find owner with that email'))) return self.respond('sentry/projects/transfer.html', context={'form': form}) transaction_id = uuid4().hex url_data = sign( actor_id=request.user.id, from_organization_id=organization.id, project_id=project.id, user_id=owner.user_id, transaction_id=transaction_id) has_new_teams = features.has( 'organizations:new-teams', organization, actor=request.user, ) context = { 'email': email, 'from_org': organization.name, 'project_name': project.slug if has_new_teams else project.name, 'request_time': timezone.now(), 'url': absolute_uri('/accept-transfer/') + '?' + urlencode({'data': url_data}), 'requester': request.user } MessageBuilder( subject='%sRequest for Project Transfer' % (options.get('mail.subject-prefix'), ), template='sentry/emails/transfer_project.txt', html_template='sentry/emails/transfer_project.html', type='org.confirm_project_transfer_request', context=context, ).send_async([email]) self.create_audit_entry( request=request, organization=project.organization, target_object=project.id, event=AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER, data=project.get_audit_log_data(), transaction_id=transaction_id, ) messages.add_message( request, messages.SUCCESS, _(u'A request was sent to move project %r to a different organization') % ((project.slug if has_new_teams else project.name).encode('utf-8'), ) ) return HttpResponseRedirect( reverse('sentry-organization-home', args=[organization.slug]) ) context = { 'form': form, } return self.respond('sentry/projects/transfer.html', context)
def test_map_params(self): config_view = MsTeamsExtensionConfigurationView() data = {"my_param": "test"} signed_data = sign(**data) params = {"signed_params": signed_data} assert data == config_view.map_params_to_state(params)