Exemplo n.º 1
0
class Destroyer(Mediator):
    install = Param(
        'sentry.models.sentryappinstallation.SentryAppInstallation')

    def call(self):
        self._destroy_authorization()
        self._destroy_grant()
        self._destroy_installation()
        return self.install

    def _destroy_authorization(self):
        self.install.authorization.delete()

    def _destroy_grant(self):
        self.install.api_grant.delete()

    def _destroy_installation(self):
        self.install.delete()
Exemplo n.º 2
0
class Creator(Mediator):
    name = Param(six.string_types)
    organization = Param('sentry.models.Organization')
    scopes = Param(Iterable)
    events = Param(Iterable, default=lambda self: [])
    webhook_url = Param(six.string_types)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    overview = Param(six.string_types, required=False)

    def call(self):
        self.proxy = self._create_proxy_user()
        self.api_app = self._create_api_application()
        self.app = self._create_sentry_app()
        return self.app

    def _create_proxy_user(self):
        return User.objects.create(
            username=self.name.lower(),
            is_sentry_app=True,
        )

    def _create_api_application(self):
        return ApiApplication.objects.create(owner_id=self.proxy.id, )

    def _create_sentry_app(self):
        from sentry.mediators.service_hooks.creator import expand_events

        return SentryApp.objects.create(
            name=self.name,
            application_id=self.api_app.id,
            owner_id=self.organization.id,
            proxy_user_id=self.proxy.id,
            scope_list=self.scopes,
            events=expand_events(self.events),
            webhook_url=self.webhook_url,
            redirect_url=self.redirect_url,
            is_alertable=self.is_alertable,
            overview=self.overview,
        )
Exemplo n.º 3
0
class Updater(Mediator):
    sentry_app = Param('sentry.models.SentryApp')
    name = Param(six.string_types, required=False)
    scopes = Param(Iterable, required=False)
    webhook_url = Param(six.string_types, required=False)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, required=False)
    overview = Param(six.string_types, required=False)

    def call(self):
        self._update_name()
        self._update_scopes()
        self._update_webhook_url()
        self._update_redirect_url()
        self._update_is_alertable()
        self._update_overview()
        self.sentry_app.save()
        return self.sentry_app

    @if_param('name')
    def _update_name(self):
        self.sentry_app.name = self.name

    @if_param('scopes')
    def _update_scopes(self):
        if self.sentry_app.status == SentryAppStatus.PUBLISHED:
            raise APIError('Cannot update scopes on published App.')
        self.sentry_app.scope_list = self.scopes

    @if_param('webhook_url')
    def _update_webhook_url(self):
        self.sentry_app.webhook_url = self.webhook_url

    @if_param('redirect_url')
    def _update_redirect_url(self):
        self.sentry_app.redirect_url = self.redirect_url

    @if_param('is_alertable')
    def _update_is_alertable(self):
        self.sentry_app.is_alertable = self.is_alertable

    @if_param('overview')
    def _update_overview(self):
        self.sentry_app.overview = self.overview
Exemplo n.º 4
0
class IssueLinkCreator(Mediator):
    install = Param("sentry.models.SentryAppInstallation")
    group = Param("sentry.models.Group")
    action = Param(six.string_types)
    fields = Param(object)
    uri = Param(six.string_types)
    user = Param("sentry.models.User")

    def call(self):
        self._verify_action()
        self._make_external_request()
        self._create_external_issue()
        return self.external_issue

    def _verify_action(self):
        if self.action not in ["link", "create"]:
            raise APIUnauthorized("Invalid action '{}'".format(self.action))

    def _make_external_request(self):
        self.response = external_requests.IssueLinkRequester.run(
            install=self.install,
            uri=self.uri,
            group=self.group,
            fields=self.fields,
            user=self.user,
            action=self.action,
        )

    def _format_response_data(self):
        web_url = self.response["webUrl"]

        display_name = "{}#{}".format(escape(self.response["project"]),
                                      escape(self.response["identifier"]))

        return [web_url, display_name]

    def _create_external_issue(self):
        web_url, display_name = self._format_response_data()
        self.external_issue = PlatformExternalIssue.objects.create(
            group_id=self.group.id,
            project_id=self.group.project_id,
            service_type=self.sentry_app.slug,
            display_name=display_name,
            web_url=web_url,
        )

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 5
0
class IssueLinkCreator(Mediator):
    install = Param('sentry.models.SentryAppInstallation')
    group = Param('sentry.models.Group')
    action = Param(six.string_types)
    fields = Param(object)
    uri = Param(six.string_types)
    user = Param('sentry.models.User')

    def call(self):
        self._verify_action()
        self._make_external_request()
        self._create_external_issue()
        return self.external_issue

    def _verify_action(self):
        if self.action not in ['link', 'create']:
            return APIUnauthorized()

    def _make_external_request(self):
        self.response = external_requests.IssueLinkRequester.run(
            install=self.install,
            uri=self.uri,
            group=self.group,
            fields=self.fields,
            user=self.user,
        )

    def _format_response_data(self):
        web_url = self.response['webUrl']

        display_name = u'{}#{}'.format(
            self.response['project'],
            self.response['identifier'],
        )

        return [web_url, display_name]

    def _create_external_issue(self):
        web_url, display_name = self._format_response_data()
        self.external_issue = PlatformExternalIssue.objects.create(
            group_id=self.group.id,
            service_type=self.sentry_app.slug,
            display_name=display_name,
            web_url=web_url,
        )

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 6
0
class Destroyer(Mediator):
    sentry_app = Param('sentry.models.SentryApp')

    def call(self):
        self._destroy_sentry_app_installations()
        self._destroy_api_application()
        self._destroy_proxy_user()
        self._destroy_sentry_app()
        return self.sentry_app

    def _destroy_sentry_app_installations(self):
        for install in self.sentry_app.installations.all():
            sentry_app_installations.Destroyer.run(install=install)

    def _destroy_api_application(self):
        self.sentry_app.application.delete()

    def _destroy_proxy_user(self):
        self.sentry_app.proxy_user.delete()

    def _destroy_sentry_app(self):
        self.sentry_app.delete()
Exemplo n.º 7
0
class Creator(Mediator):
    name = Param(six.string_types)
    organization = Param('sentry.models.Organization')
    scopes = Param(Iterable)
    webhook_url = Param(six.string_types)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    overview = Param(six.string_types, required=False)

    def call(self):
        self.proxy = self._create_proxy_user()
        self.api_app = self._create_api_application()
        self.app = self._create_sentry_app()
        return self.app

    def _create_proxy_user(self):
        return User.objects.create(
            username=self.name.lower(),
            is_sentry_app=True,
        )

    def _create_api_application(self):
        return ApiApplication.objects.create(
            owner=self.proxy,
        )

    def _create_sentry_app(self):
        return SentryApp.objects.create(
            name=self.name,
            application=self.api_app,
            owner=self.organization,
            proxy_user=self.proxy,
            scope_list=self.scopes,
            webhook_url=self.webhook_url,
            redirect_url=self.redirect_url,
            is_alertable=self.is_alertable,
            overview=self.overview,
        )
Exemplo n.º 8
0
class IssueLinkCreator(Mediator):
    install = Param("sentry.models.SentryAppInstallation")
    group = Param("sentry.models.Group")
    action = Param((str, ))
    fields = Param(object)
    uri = Param((str, ))
    user = Param("sentry.models.User")

    def call(self):
        self._verify_action()
        self._make_external_request()
        self._create_external_issue()
        return self.external_issue

    def _verify_action(self):
        if self.action not in ["link", "create"]:
            raise APIUnauthorized("Invalid action '{}'".format(self.action))

    def _make_external_request(self):
        self.response = external_requests.IssueLinkRequester.run(
            install=self.install,
            uri=self.uri,
            group=self.group,
            fields=self.fields,
            user=self.user,
            action=self.action,
        )

    def _create_external_issue(self):
        self.external_issue = external_issues.Creator.run(
            install=self.install,
            group=self.group,
            web_url=self.response["webUrl"],
            project=self.response["project"],
            identifier=self.response["identifier"],
        )

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 9
0
class Creator(Mediator):
    application = Param('sentry.models.ApiApplication', required=False)
    actor = Param('sentry.db.models.BaseModel')
    organization = Param('sentry.models.Organization')
    projects = Param(Iterable)
    events = Param(Iterable)
    url = Param(six.string_types)

    def call(self):
        self.hook = self._create_service_hook()
        return self.hook

    def _create_service_hook(self):
        application_id = self.application.id if self.application else None

        # For Sentry Apps, if projects = [], the service hook applies to all
        # the projects in the organization.
        # We are using the first project so that we can satisfy the not null
        # contraint for project_id on the ServiceHook table.
        #
        # Otherwise, we'll always have a single project passed through by
        # the ProjectServiceHooksEndpoint
        if not self.projects:
            project_id = self.organization.project_set.first().id
        else:
            project_id = self.projects[0].id

        hook = ServiceHook.objects.create(
            application_id=application_id,
            actor_id=self.actor.id,
            project_id=project_id,
            organization_id=self.organization.id,
            events=expand_events(self.events),
            url=self.url,
        )
        for project in self.projects:
            hook.add_project(project)

        return hook
Exemplo n.º 10
0
class Preparer(Mediator):
    component = Param("sentry.models.SentryAppComponent")
    install = Param("sentry.models.SentryAppInstallation")
    project = Param("sentry.models.Project", required=False, default=None)
    values = Param(dict, required=False, default=[])

    def call(self):
        if self.component.type == "issue-link":
            return self._prepare_issue_link()
        if self.component.type == "stacktrace-link":
            return self._prepare_stacktrace_link()
        if self.component.type == "alert-rule-action":
            return self._prepare_alert_rule_action()

    def _prepare_stacktrace_link(self):
        schema = self.component.schema
        uri = schema.get("uri")

        urlparts = list(
            urlparse(force_str(self.install.sentry_app.webhook_url)))
        urlparts[2] = uri

        query = {"installationId": self.install.uuid}

        if self.project:
            query["projectSlug"] = self.project.slug

        urlparts[4] = urlencode(query)
        schema.update({"url": urlunparse(urlparts)})

    def _prepare_issue_link(self):
        schema = self.component.schema.copy()

        link = schema.get("link", {})
        create = schema.get("create", {})

        for field in link.get("required_fields", []):
            self._prepare_field(field)

        for field in link.get("optional_fields", []):
            self._prepare_field(field)

        for field in create.get("required_fields", []):
            self._prepare_field(field)

        for field in create.get("optional_fields", []):
            self._prepare_field(field)

    def _prepare_alert_rule_action(self):
        schema = self.component.schema.copy()
        settings = schema.get("settings", {})

        for field in settings.get("required_fields", []):
            self._prepare_field(field)

        for field in settings.get("optional_fields", []):
            self._prepare_field(field)

    def _prepare_field(self, field):
        if "depends_on" in field:
            dependant_data_list = list(
                filter(lambda val: val["name"] in field.get("depends_on", []),
                       self.values))
            if len(dependant_data_list) != len(field.get("depends_on")):
                return field.update({"choices": []})

            dependant_data = json.dumps(
                {x["name"]: x["value"]
                 for x in dependant_data_list})

            return self._get_select_choices(field, dependant_data)

        return self._get_select_choices(field)

    def _get_select_choices(self, field, dependant_data=None):
        if "options" in field:
            return field.update({"choices": field["options"]})

        if "uri" in field:
            if not field.get("skip_load_on_open"):
                return field.update(
                    self._request(field["uri"], dependent_data=dependant_data))

    def _request(self, uri, dependent_data=None):
        return SelectRequester.run(install=self.install,
                                   project=self.project,
                                   uri=uri,
                                   dependent_data=dependent_data)
Exemplo n.º 11
0
class Creator(Mediator):
    name = Param(six.string_types)
    author = Param(six.string_types)
    organization = Param("sentry.models.Organization")
    scopes = Param(Iterable, default=lambda self: [])
    events = Param(Iterable, default=lambda self: [])
    webhook_url = Param(
        six.string_types, required=False
    )  # only not required for internal integrations but internalCreator calls this
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    verify_install = Param(bool, default=True)
    schema = Param(dict, default=lambda self: {})
    overview = Param(six.string_types, required=False)
    allowed_origins = Param(Iterable, default=lambda self: [])
    request = Param("rest_framework.request.Request", required=False)
    user = Param("sentry.models.User")
    is_internal = Param(bool)

    def call(self):
        self.slug = self._generate_and_validate_slug()
        self.proxy = self._create_proxy_user()
        self.api_app = self._create_api_application()
        self.sentry_app = self._create_sentry_app()
        self._create_ui_components()
        self._create_integration_feature()
        return self.sentry_app

    def _generate_and_validate_slug(self):
        slug = generate_slug(self.name, is_internal=self.is_internal)

        # validate globally unique slug
        queryset = SentryApp.with_deleted.filter(slug=slug)

        if queryset.exists():
            # In reality, the slug is taken but it's determined by the name field
            raise ValidationError(
                {"name": [u"Name {} is already taken, please use another.".format(self.name)]}
            )
        return slug

    def _create_proxy_user(self):
        # need a proxy user name that will always be unique
        return User.objects.create(
            username=u"{}-{}".format(self.slug, default_uuid()), is_sentry_app=True
        )

    def _create_api_application(self):
        return ApiApplication.objects.create(
            owner_id=self.proxy.id, allowed_origins="\n".join(self.allowed_origins)
        )

    def _create_sentry_app(self):
        from sentry.mediators.service_hooks.creator import expand_events

        kwargs = {
            "name": self.name,
            "slug": self.slug,
            "author": self.author,
            "application_id": self.api_app.id,
            "owner_id": self.organization.id,
            "proxy_user_id": self.proxy.id,
            "scope_list": self.scopes,
            "events": expand_events(self.events),
            "schema": self.schema or {},
            "webhook_url": self.webhook_url,
            "redirect_url": self.redirect_url,
            "is_alertable": self.is_alertable,
            "verify_install": self.verify_install,
            "overview": self.overview,
        }

        if self.is_internal:
            kwargs["status"] = SentryAppStatus.INTERNAL

        return SentryApp.objects.create(**kwargs)

    def _create_ui_components(self):
        schema = self.schema or {}

        for element in schema.get("elements", []):
            SentryAppComponent.objects.create(
                type=element["type"], sentry_app_id=self.sentry_app.id, schema=element
            )

    def _create_integration_feature(self):
        # sentry apps must have at least one feature
        # defaults to 'integrations-api'
        try:
            with transaction.atomic():
                IntegrationFeature.objects.create(sentry_app=self.sentry_app)
        except IntegrityError as e:
            self.log(sentry_app=self.sentry_app.slug, error_message=six.text_type(e))

    def audit(self):
        from sentry.utils.audit import create_audit_entry

        if self.request:
            create_audit_entry(
                request=self.request,
                organization=self.organization,
                target_object=self.organization.id,
                event=AuditLogEntryEvent.SENTRY_APP_ADD,
                data={"sentry_app": self.sentry_app.name},
            )

    def record_analytics(self):
        analytics.record(
            "sentry_app.created",
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app=self.sentry_app.slug,
        )
Exemplo n.º 12
0
class IssueLinkRequester(Mediator):
    """
    1. Makes a POST request to another service with data used for creating or
    linking a Sentry issue to an issue in the other service.

    The data sent to the other service is always in the following format:
        {
            'installtionId': <install_uuid>,
            'issueId': <sentry_group_id>,
            'webUrl': <sentry_group_web_url>,
            <fields>,
        }

    <fields> are any of the 'create' or 'link' form fields (determined by
    the schema for that particular service)

    2. Validates the response format from the other service and returns the
    payload.

    The data sent to the other service is always in the following format:
        {
            'identifier': <some_identifier>,
            'webUrl': <external_issue_web_url>,
            'project': <top_level_identifier>,
        }

    The project and identifier are use to generate the display text for the linked
    issue in the UI (i.e. <project>#<identifier>)
    """

    install = Param('sentry.models.SentryAppInstallation')
    uri = Param(six.string_types)
    group = Param('sentry.models.Group')
    fields = Param(object)

    def call(self):
        return self._make_request()

    def _build_url(self):
        domain = urlparse(self.sentry_app.webhook_url).netloc
        url = u'https://{}{}'.format(domain, self.uri)
        return url

    def _make_request(self):
        req = safe_urlopen(
            url=self._build_url(),
            headers=self._build_headers(),
            method='POST',
            data=self.body,
        )

        try:
            body = safe_urlread(req)
            response = json.loads(body)
        except Exception:
            logger.info('issue-link-requester.error',
                        extra={
                            'sentry_app': self.sentry_app.slug,
                            'install': self.install.uuid,
                            'project': self.group.project.slug,
                            'group': self.group.id,
                            'uri': self.uri,
                        })
            response = {}

        if not self._validate_response(response):
            raise APIError()

        return response

    def _validate_response(self, resp):
        return validate(instance=resp, schema_type='issue_link')

    def _build_headers(self):
        request_uuid = uuid4().hex

        return {
            'Content-Type': 'application/json',
            'Request-ID': request_uuid,
            'Sentry-App-Signature': self.sentry_app.build_signature(self.body)
        }

    @memoize
    def body(self):
        body = {}
        for name, value in six.iteritems(self.fields):
            body[name] = value

        body['issueId'] = self.group.id
        body['installationId'] = self.install.uuid
        body['webUrl'] = self.group.get_absolute_url()
        return json.dumps(body)

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 13
0
class Updater(Mediator, SentryAppMixin):
    sentry_app = Param("sentry.models.SentryApp")
    name = Param((str,), required=False)
    status = Param((str,), required=False)
    scopes = Param(Iterable, required=False)
    events = Param(Iterable, required=False)
    webhook_url = Param((str,), required=False)
    redirect_url = Param((str,), required=False)
    is_alertable = Param(bool, required=False)
    verify_install = Param(bool, required=False)
    schema = Param(dict, required=False)
    overview = Param((str,), required=False)
    allowed_origins = Param(Iterable, required=False)
    popularity = Param(int, required=False)
    user = Param("sentry.models.User")
    features = Param(Iterable, required=False)

    def call(self):
        self._update_name()
        self._update_author()
        self._update_features()
        self._update_status()
        self._update_scopes()
        self._update_events()
        self._update_webhook_url()
        self._update_redirect_url()
        self._update_is_alertable()
        self._update_verify_install()
        self._update_overview()
        self._update_allowed_origins()
        self._update_schema()
        self._update_service_hooks()
        self._update_popularity()
        self.sentry_app.save()
        return self.sentry_app

    @if_param("features")
    def _update_features(self):
        if not self.user.is_superuser and self.sentry_app.status == SentryAppStatus.PUBLISHED:
            raise APIError("Cannot update features on a published integration.")

        IntegrationFeature.objects.clean_update(
            incoming_features=list(self.features),
            target=self.sentry_app,
            target_type=IntegrationTypes.SENTRY_APP,
        )

    @if_param("name")
    def _update_name(self):
        self.sentry_app.name = self.name

    @if_param("author")
    def _update_author(self):
        self.sentry_app.author = self.author

    @if_param("status")
    def _update_status(self):
        if self.user.is_superuser:
            if self.status == SentryAppStatus.PUBLISHED_STR:
                self.sentry_app.status = SentryAppStatus.PUBLISHED
                self.sentry_app.date_published = timezone.now()
            if self.status == SentryAppStatus.UNPUBLISHED_STR:
                self.sentry_app.status = SentryAppStatus.UNPUBLISHED
        if self.status == SentryAppStatus.PUBLISH_REQUEST_INPROGRESS_STR:
            self.sentry_app.status = SentryAppStatus.PUBLISH_REQUEST_INPROGRESS

    @if_param("scopes")
    def _update_scopes(self):
        if (
            self.sentry_app.status == SentryAppStatus.PUBLISHED
            and self.sentry_app.scope_list != self.scopes
        ):
            raise APIError("Cannot update permissions on a published integration.")
        self.sentry_app.scope_list = self.scopes
        # update the scopes of active tokens tokens
        ApiToken.objects.filter(
            Q(expires_at__isnull=True) | Q(expires_at__gt=timezone.now()),
            application=self.sentry_app.application,
        ).update(scope_list=list(self.scopes))

    @if_param("events")
    def _update_events(self):
        for event in self.events:
            needed_scope = REQUIRED_EVENT_PERMISSIONS[event]
            if needed_scope not in self.sentry_app.scope_list:
                raise APIError(f"{event} webhooks require the {needed_scope} permission.")

        from sentry.mediators.service_hooks.creator import expand_events

        self.sentry_app.events = expand_events(self.events)

    def _update_service_hooks(self):
        hooks = ServiceHook.objects.filter(application=self.sentry_app.application)
        # sentry_app.webhook_url will be updated at this point
        webhook_url = self.sentry_app.webhook_url
        for hook in hooks:
            # update the url and events
            if webhook_url:
                service_hooks.Updater.run(
                    service_hook=hook, events=self.sentry_app.events, url=webhook_url
                )
            # if no url, then the service hook is no longer active in which case we need to delete it
            else:
                service_hooks.Destroyer.run(service_hook=hook)
        # if we don't have hooks but we have a webhook url now, need to create it for an internal integration
        if webhook_url and self.sentry_app.is_internal and not hooks:
            installation = SentryAppInstallation.objects.get(sentry_app_id=self.sentry_app.id)
            service_hooks.Creator.run(
                application=self.sentry_app.application,
                actor=installation,
                projects=[],
                organization=self.sentry_app.owner,
                events=self.sentry_app.events,
                url=webhook_url,
            )

    @if_param("webhook_url")
    def _update_webhook_url(self):
        self.sentry_app.webhook_url = self.webhook_url

    @if_param("redirect_url")
    def _update_redirect_url(self):
        self.sentry_app.redirect_url = self.redirect_url

    @if_param("is_alertable")
    def _update_is_alertable(self):
        self.sentry_app.is_alertable = self.is_alertable

    @if_param("verify_install")
    def _update_verify_install(self):
        if self.sentry_app.is_internal and self.verify_install:
            raise APIError("Internal integrations cannot have verify_install=True.")
        self.sentry_app.verify_install = self.verify_install

    @if_param("overview")
    def _update_overview(self):
        self.sentry_app.overview = self.overview

    @if_param("allowed_origins")
    def _update_allowed_origins(self):
        self.sentry_app.application.allowed_origins = "\n".join(self.allowed_origins)
        self.sentry_app.application.save()

    @if_param("popularity")
    def _update_popularity(self):
        if self.user.is_superuser:
            self.sentry_app.popularity = self.popularity

    @if_param("schema")
    def _update_schema(self):
        self.sentry_app.schema = self.schema
        self.new_schema_elements = self._get_new_schema_elements()
        self._delete_old_ui_components()
        self._create_ui_components()

    def _get_new_schema_elements(self) -> Set[str]:
        current = SentryAppComponent.objects.filter(sentry_app=self.sentry_app).values_list(
            "type", flat=True
        )
        return self.get_schema_types() - set(current)

    def _delete_old_ui_components(self):
        SentryAppComponent.objects.filter(sentry_app_id=self.sentry_app.id).delete()

    def _create_ui_components(self):
        for element in self.schema.get("elements", []):
            SentryAppComponent.objects.create(
                type=element["type"], sentry_app_id=self.sentry_app.id, schema=element
            )

    def record_analytics(self):
        analytics.record(
            "sentry_app.updated",
            user_id=self.user.id,
            organization_id=self.sentry_app.owner_id,
            sentry_app=self.sentry_app.slug,
            created_alert_rule_ui_component="alert-rule-action" in (self.new_schema_elements or {}),
        )
Exemplo n.º 14
0
class Creator(Mediator):
    sentry_app_installation = Param('sentry.models.SentryAppInstallation')
    expires_at = Param(datetime.date, default=None, required=False)
    # analytics and audit params
    generate_audit = Param(bool, default=False)
    user = Param('sentry.models.User')
    request = Param('rest_framework.request.Request', required=False)

    def call(self):
        self._check_token_limit()
        self._create_api_token()
        self._create_sentry_app_installation_token()
        return self.api_token

    def _check_token_limit(self):
        curr_count = SentryAppInstallationToken.objects.filter(
            sentry_app_installation=self.sentry_app_installation).count()
        if curr_count >= INTERNAL_INTEGRATION_TOKEN_COUNT_MAX:
            raise ApiTokenLimitError(
                'Cannot generate more than %d tokens for a single integration'
                % INTERNAL_INTEGRATION_TOKEN_COUNT_MAX)

    def _create_api_token(self):
        self.api_token = ApiToken.objects.create(
            user=self.sentry_app.proxy_user,
            application_id=self.sentry_app.application.id,
            scope_list=self.sentry_app.scope_list,
            expires_at=self.expires_at,
        )

    def _create_sentry_app_installation_token(self):
        self.sentry_app_installation_token = SentryAppInstallationToken.objects.create(
            api_token=self.api_token,
            sentry_app_installation=self.sentry_app_installation)

    def audit(self):
        from sentry.utils.audit import create_audit_entry
        if self.request and self.generate_audit:
            create_audit_entry(
                request=self.request,
                organization=self.organization,
                target_object=self.api_token.id,
                event=AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD_TOKEN,
                data={'sentry_app': self.sentry_app.name},
            )

    def record_analytics(self):
        from sentry import analytics
        analytics.record(
            'sentry_app_installation_token.created',
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app_installation_id=self.sentry_app_installation.id,
            sentry_app=self.sentry_app.slug,
        )

    @memoize
    def sentry_app(self):
        return self.sentry_app_installation.sentry_app

    @memoize
    def organization(self):
        return self.sentry_app_installation.organization
Exemplo n.º 15
0
 def test_validate_user_defined_type(self):
     user = Param("sentry.models.User")
     assert user.validate(None, "user", User())
class AlertRuleActionRequester(Mediator):
    """
    Makes a POST request to another service to fetch/update the values for each field in the
    AlertRuleAction settings schema
    """

    install = Param("sentry.models.SentryAppInstallation")
    uri = Param((str, ))
    fields = Param(object, required=False, default={})
    http_method = Param(str, required=False, default="POST")

    def call(self):
        return self._make_request()

    def _build_url(self):
        urlparts = list(urlparse(self.sentry_app.webhook_url))
        urlparts[2] = self.uri
        return urlunparse(urlparts)

    def _make_request(self):
        try:
            req = send_and_save_sentry_app_request(
                self._build_url(),
                self.sentry_app,
                self.install.organization_id,
                "alert_rule_action.requested",
                headers=self._build_headers(),
                method=self.http_method,
                data=self.body,
            )
            body = safe_urlread(req)
            response = {
                "success": True,
                "message": "",
                "body": json.loads(body)
            }
        except Exception as e:
            logger.info(
                "alert_rule_action.error",
                extra={
                    "sentry_app_slug": self.sentry_app.slug,
                    "install_uuid": self.install.uuid,
                    "uri": self.uri,
                    "error_message": str(e),
                },
            )
            # Bubble up error message from Sentry App to the UI for the user.
            response = {
                "success": False,
                "message": str(e.response.text),
                "body": {}
            }

        return response

    def _build_headers(self):
        request_uuid = uuid4().hex

        return {
            "Content-Type": "application/json",
            "Request-ID": request_uuid,
            "Sentry-App-Signature": self.sentry_app.build_signature(""),
        }

    @memoize
    def body(self):
        body = {"fields": {}}
        for name, value in self.fields.items():
            body["fields"][name] = value

        body["installationId"] = self.install.uuid

        return json.dumps(body)

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 17
0
class SelectRequester(Mediator):
    """
    1. Makes a GET request to another service to fetch data needed to populate
    the SelectField dropdown in the UI.

    `installationId` and `project` are included in the params of the request

    2. Validates and formats the response.
    """

    install = Param('sentry.models.SentryAppInstallation')
    project = Param('sentry.models.Project', required=False)
    uri = Param(six.string_types)
    query = Param(six.string_types, required=False)

    def call(self):
        return self._make_request()

    def _build_url(self):
        urlparts = list(urlparse(self.sentry_app.webhook_url))
        urlparts[2] = self.uri

        query = {'installationId': self.install.uuid}

        if self.project:
            query['projectSlug'] = self.project.slug

        if self.query:
            query['query'] = self.query

        urlparts[4] = urlencode(query)
        return urlunparse(urlparts)

    def _make_request(self):
        try:
            body = safe_urlread(
                safe_urlopen(
                    url=self._build_url(),
                    headers=self._build_headers(),
                ))

            response = json.loads(body)
        except Exception as e:
            logger.info('select-requester.error',
                        extra={
                            'sentry_app': self.sentry_app.slug,
                            'install': self.install.uuid,
                            'project': self.project and self.project.slug,
                            'uri': self.uri,
                            'error_message': e.message,
                        })
            response = {}

        if not self._validate_response(response):
            raise APIError()

        return self._format_response(response)

    def _validate_response(self, resp):
        return validate(instance=resp, schema_type='select')

    def _format_response(self, resp):
        # the UI expects the following form:
        # choices: [[label, value]]
        # default: [label, value]
        response = {}
        choices = []

        for option in resp:
            choices.append([option['value'], option['label']])
            if option.get('default'):
                response['defaultValue'] = option['value']

        response['choices'] = choices
        return response

    def _build_headers(self):
        request_uuid = uuid4().hex

        return {
            'Content-Type': 'application/json',
            'Request-ID': request_uuid,
            'Sentry-App-Signature': self.sentry_app.build_signature('')
        }

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 18
0
class Updater(Mediator):
    rule = Param("sentry.models.Rule")
    name = Param((str, ), required=False)
    owner = Param("sentry.models.Actor", required=False)
    environment = Param(int, required=False)
    project = Param("sentry.models.Project")
    action_match = Param((str, ), required=False)
    filter_match = Param((str, ), required=False)
    actions = Param(Iterable, required=False)
    conditions = Param(Iterable, required=False)
    frequency = Param(int, required=False)
    request = Param("rest_framework.request.Request", required=False)

    def call(self):
        self._update_name()
        self._update_owner()
        self._update_environment()
        self._update_project()
        self._update_actions()
        self._update_action_match()
        self._update_filter_match()
        self._update_conditions()
        self._update_frequency()
        self.rule.save()
        return self.rule

    @if_param("name")
    def _update_name(self):
        self.rule.label = self.name

    def _update_owner(self):
        self.rule.owner = Actor.objects.get(
            id=self.owner) if self.owner else None

    def _update_environment(self):
        # environment can be None so we don't use the if_param decorator
        self.rule.environment_id = self.environment

    @if_param("project")
    def _update_project(self):
        self.rule.project = self.project

    @if_param("actions")
    def _update_actions(self):
        self.rule.data["actions"] = self.actions

    @if_param("action_match")
    def _update_action_match(self):
        self.rule.data["action_match"] = self.action_match

    @if_param("filter_match")
    def _update_filter_match(self):
        self.rule.data["filter_match"] = self.filter_match

    @if_param("conditions")
    def _update_conditions(self):
        self.rule.data["conditions"] = self.conditions

    @if_param("frequency")
    def _update_frequency(self):
        self.rule.data["frequency"] = self.frequency
Exemplo n.º 19
0
class InternalCreator(Mediator):
    name = Param(six.string_types)
    organization = Param("sentry.models.Organization")
    scopes = Param(Iterable, default=lambda self: [])
    events = Param(Iterable, default=lambda self: [])
    webhook_url = Param(six.string_types, required=False)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    schema = Param(dict, default=lambda self: {})
    overview = Param(six.string_types, required=False)
    allowed_origins = Param(Iterable, default=lambda self: [])
    request = Param("rest_framework.request.Request", required=False)
    user = Param("sentry.models.User")

    def call(self):
        # SentryAppCreator expects an author so just set it to the org name
        self.kwargs["author"] = self.organization.name
        self.kwargs["is_internal"] = True
        self.sentry_app = SentryAppCreator.run(**self.kwargs)
        self.sentry_app.verify_install = False
        self.sentry_app.save()

        self._install()
        self._create_access_token()

        return self.sentry_app

    def _create_access_token(self):
        data = {"sentry_app_installation": self.install, "user": self.user}

        self.install.api_token = SentryAppInstallationTokenCreator.run(
            request=self.request, **data)
        self.install.save()

    def _install(self):
        self.install = InstallationCreator.run(
            organization=self.organization,
            slug=self.sentry_app.slug,
            user=self.user,
            request=self.request,
            notify=False,
        )

    def audit(self):
        from sentry.utils.audit import create_audit_entry

        if self.request:
            create_audit_entry(
                request=self.request,
                organization=self.organization,
                target_object=self.organization.id,
                event=AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD,
                data={"name": self.sentry_app.name},
            )

    def record_analytics(self):
        from sentry import analytics

        analytics.record(
            "internal_integration.created",
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app=self.sentry_app.slug,
        )
Exemplo n.º 20
0
    def test_validate_type(self):
        name = Param((str, ))

        with self.assertRaises(TypeError):
            name.validate(None, "name", 1)
Exemplo n.º 21
0
 def test_lambda_default(self):
     _name = "Steve"
     name = Param((str, ), default=lambda self: _name)
     assert name.default(None) == "Steve"
Exemplo n.º 22
0
 def test_default(self):
     name = Param((str, ), default="Pete")
     assert name.default(None) == "Pete"
Exemplo n.º 23
0
class IssueLinkRequester(Mediator):
    """
    1. Makes a POST request to another service with data used for creating or
    linking a Sentry issue to an issue in the other service.

    The data sent to the other service is always in the following format:
        {
            'installationId': <install_uuid>,
            'issueId': <sentry_group_id>,
            'webUrl': <sentry_group_web_url>,
            <fields>,
        }

    <fields> are any of the 'create' or 'link' form fields (determined by
    the schema for that particular service)

    2. Validates the response format from the other service and returns the
    payload.

    The data sent to the other service is always in the following format:
        {
            'identifier': <some_identifier>,
            'webUrl': <external_issue_web_url>,
            'project': <top_level_identifier>,
        }

    The project and identifier are use to generate the display text for the linked
    issue in the UI (i.e. <project>#<identifier>)
    """

    install = Param("sentry.models.SentryAppInstallation")
    uri = Param((str, ))
    group = Param("sentry.models.Group")
    fields = Param(object)
    user = Param("sentry.models.User")
    action = Param((str, ))

    def call(self):
        return self._make_request()

    def _build_url(self):
        urlparts = urlparse(self.sentry_app.webhook_url)
        return f"{urlparts.scheme}://{urlparts.netloc}{self.uri}"

    def _make_request(self):
        action_to_past_tense = {"create": "created", "link": "linked"}

        try:
            req = send_and_save_sentry_app_request(
                self._build_url(),
                self.sentry_app,
                self.install.organization_id,
                "external_issue.{}".format(action_to_past_tense[self.action]),
                headers=self._build_headers(),
                method="POST",
                data=self.body,
            )
            body = safe_urlread(req)
            response = json.loads(body)
        except Exception as e:
            logger.info(
                "issue-link-requester.error",
                extra={
                    "sentry_app": self.sentry_app.slug,
                    "install": self.install.uuid,
                    "project": self.group.project.slug,
                    "group": self.group.id,
                    "uri": self.uri,
                    "error_message": str(e),
                },
            )
            response = {}

        if not self._validate_response(response):
            raise APIError()

        return response

    def _validate_response(self, resp):
        return validate(instance=resp, schema_type="issue_link")

    def _build_headers(self):
        request_uuid = uuid4().hex

        return {
            "Content-Type": "application/json",
            "Request-ID": request_uuid,
            "Sentry-App-Signature": self.sentry_app.build_signature(self.body),
        }

    @memoize
    def body(self):
        body = {"fields": {}}
        for name, value in self.fields.items():
            body["fields"][name] = value

        body["issueId"] = self.group.id
        body["installationId"] = self.install.uuid
        body["webUrl"] = self.group.get_absolute_url()
        project = self.group.project
        body["project"] = {"slug": project.slug, "id": project.id}
        body["actor"] = {
            "type": "user",
            "id": self.user.id,
            "name": self.user.name
        }
        return json.dumps(body)

    @memoize
    def sentry_app(self):
        return self.install.sentry_app
Exemplo n.º 24
0
class GrantExchanger(Mediator):
    """
    Exchanges a Grant Code for an Access Token
    """
    install = Param('sentry.models.SentryAppInstallation')
    code = Param(six.string_types)
    client_id = Param(six.string_types)
    user = Param('sentry.models.User')

    def call(self):
        self._validate()
        self._create_token()

        # Once it's exchanged it's no longer valid and should not be
        # exchangable, so we delete it.
        self._delete_grant()

        return self.token

    def record_analytics(self):
        analytics.record(
            'sentry_app.token_exchanged',
            sentry_app_installation_id=self.install.id,
            exchange_type='authorization',
        )

    def _validate(self):
        Validator.run(
            install=self.install,
            client_id=self.client_id,
            user=self.user,
        )

        if (not self._grant_belongs_to_install()
                or not self._sentry_app_user_owns_grant()):
            raise APIUnauthorized

        if not self._grant_is_active():
            raise APIUnauthorized('Grant has already expired.')

    def _grant_belongs_to_install(self):
        return self.grant.sentry_app_installation == self.install

    def _sentry_app_user_owns_grant(self):
        return self.grant.application.owner == self.user

    def _grant_is_active(self):
        return self.grant.expires_at > datetime.now(pytz.UTC)

    def _delete_grant(self):
        self.grant.delete()

    def _create_token(self):
        self.token = ApiToken.objects.create(
            user=self.user,
            application=self.application,
            scope_list=self.sentry_app.scope_list,
            expires_at=token_expiration())
        self.install.api_token = self.token
        self.install.save()

    @memoize
    def grant(self):
        try:
            return ApiGrant.objects \
                .select_related('sentry_app_installation') \
                .select_related('application') \
                .select_related('application__sentry_app') \
                .get(code=self.code)
        except ApiGrant.DoesNotExist:
            raise APIUnauthorized

    @property
    def application(self):
        try:
            return self.grant.application
        except ApiApplication.DoesNotExist:
            raise APIUnauthorized

    @property
    def sentry_app(self):
        try:
            return self.application.sentry_app
        except SentryApp.DoesNotExist:
            raise APIUnauthorized
Exemplo n.º 25
0
class Creator(Mediator):
    organization = Param('sentry.models.Organization')
    slug = Param(six.string_types)
    user = Param('sentry.models.User')
    request = Param('rest_framework.request.Request', required=False)
    notify = Param(bool, default=True)

    def call(self):
        self._create_api_grant()
        self._create_install()
        self._create_service_hooks()
        self._notify_service()
        self.install.is_new = True
        return self.install

    def _create_install(self):
        status = SentryAppInstallationStatus.PENDING
        if not self.sentry_app.verify_install:
            status = SentryAppInstallationStatus.INSTALLED

        self.install = SentryAppInstallation.objects.create(
            organization_id=self.organization.id,
            sentry_app_id=self.sentry_app.id,
            api_grant_id=self.api_grant.id,
            status=status,
        )

    def _create_api_grant(self):
        self.api_grant = ApiGrant.objects.create(
            user_id=self.sentry_app.proxy_user.id,
            application_id=self.api_application.id,
        )

    def _create_service_hooks(self):
        service_hooks.Creator.run(
            application=self.api_application,
            actor=self.install,
            projects=[],
            organization=self.organization,
            events=self.sentry_app.events,
            url=self.sentry_app.webhook_url,
        )

    def _notify_service(self):
        if self.notify:
            installation_webhook.delay(self.install.id, self.user.id)

    def audit(self):
        from sentry.utils.audit import create_audit_entry
        if self.request:
            create_audit_entry(
                request=self.request,
                organization=self.install.organization,
                target_object=self.install.organization.id,
                event=AuditLogEntryEvent.SENTRY_APP_INSTALL,
                data={
                    'sentry_app': self.sentry_app.name,
                },
            )

    def record_analytics(self):
        analytics.record(
            'sentry_app.installed',
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app=self.slug,
        )

    @memoize
    def api_application(self):
        return self.sentry_app.application

    @memoize
    def sentry_app(self):
        return SentryApp.objects.get(slug=self.slug)
Exemplo n.º 26
0
    def test_validate_required(self):
        name = Param((str, ))

        with self.assertRaises(AttributeError):
            name.validate(None, "name", None)
Exemplo n.º 27
0
class Creator(Mediator):
    name = Param(six.string_types)
    author = Param(six.string_types)
    organization = Param('sentry.models.Organization')
    scopes = Param(Iterable, default=lambda self: [])
    events = Param(Iterable, default=lambda self: [])
    webhook_url = Param(six.string_types)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    schema = Param(dict, default=lambda self: {})
    overview = Param(six.string_types, required=False)
    request = Param('rest_framework.request.Request', required=False)
    user = Param('sentry.models.User')

    def call(self):
        self.proxy = self._create_proxy_user()
        self.api_app = self._create_api_application()
        self.sentry_app = self._create_sentry_app()
        self._create_ui_components()
        self._create_integration_feature()
        return self.sentry_app

    def _create_proxy_user(self):
        return User.objects.create(
            username=self.name.lower(),
            is_sentry_app=True,
        )

    def _create_api_application(self):
        return ApiApplication.objects.create(owner_id=self.proxy.id, )

    def _create_sentry_app(self):
        from sentry.mediators.service_hooks.creator import expand_events

        return SentryApp.objects.create(
            name=self.name,
            author=self.author,
            application_id=self.api_app.id,
            owner_id=self.organization.id,
            proxy_user_id=self.proxy.id,
            scope_list=self.scopes,
            events=expand_events(self.events),
            schema=self.schema or {},
            webhook_url=self.webhook_url,
            redirect_url=self.redirect_url,
            is_alertable=self.is_alertable,
            overview=self.overview,
        )

    def _create_ui_components(self):
        schema = self.schema or {}

        for element in schema.get('elements', []):
            SentryAppComponent.objects.create(
                type=element['type'],
                sentry_app_id=self.sentry_app.id,
                schema=element,
            )

    def _create_integration_feature(self):
        # sentry apps must have at least one feature
        # defaults to 'integrations-api'
        try:
            with transaction.atomic():
                IntegrationFeature.objects.create(sentry_app=self.sentry_app, )
        except IntegrityError as e:
            self.log(
                sentry_app=self.sentry_app.slug,
                error_message=e.message,
            )

    def audit(self):
        from sentry.utils.audit import create_audit_entry
        if self.request:
            create_audit_entry(
                request=self.request,
                organization=self.organization,
                target_object=self.organization.id,
                event=AuditLogEntryEvent.SENTRY_APP_ADD,
                data={
                    'sentry_app': self.sentry_app.name,
                },
            )

    def record_analytics(self):
        analytics.record(
            'sentry_app.created',
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app=self.sentry_app.slug,
        )
Exemplo n.º 28
0
    def test_validate_default_type(self):
        name = Param((str, ), default=1)

        with self.assertRaises(TypeError):
            name.validate(None, "name", None)
Exemplo n.º 29
0
class InternalCreator(Mediator):
    name = Param(six.string_types)
    author = Param(six.string_types)
    organization = Param('sentry.models.Organization')
    scopes = Param(Iterable, default=lambda self: [])
    events = Param(Iterable, default=lambda self: [])
    webhook_url = Param(six.string_types)
    redirect_url = Param(six.string_types, required=False)
    is_alertable = Param(bool, default=False)
    schema = Param(dict, default=lambda self: {})
    overview = Param(six.string_types, required=False)
    request = Param('rest_framework.request.Request', required=False)
    user = Param('sentry.models.User')

    def call(self):
        self.sentry_app = SentryAppCreator.run(**self.kwargs)
        self.sentry_app.status = SentryAppStatus.INTERNAL
        self.sentry_app.verify_install = False
        self.sentry_app.save()

        self._install()
        self._create_access_token()

        return self.sentry_app

    def _create_access_token(self):
        data = {'sentry_app_installation': self.install, 'user': self.user}

        self.install.api_token = SentryAppInstallationTokenCreator.run(
            request=self.request, **data)
        self.install.save()

    def _install(self):
        self.install = InstallationCreator.run(
            organization=self.organization,
            slug=self.sentry_app.slug,
            user=self.user,
            request=self.request,
            notify=False,
        )

    def audit(self):
        from sentry.utils.audit import create_audit_entry
        if self.request:
            create_audit_entry(
                request=self.request,
                organization=self.organization,
                target_object=self.organization.id,
                event=AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD,
            )

    def record_analytics(self):
        from sentry import analytics
        analytics.record(
            'internal_integration.created',
            user_id=self.user.id,
            organization_id=self.organization.id,
            sentry_app=self.sentry_app.slug,
        )
Exemplo n.º 30
0
class SelectRequester(Mediator):
    """
    1. Makes a GET request to another service to fetch data needed to populate
    the SelectField dropdown in the UI.

    `installationId` and `project` are included in the params of the request

    2. Validates and formats the response.
    """

    install = Param("sentry.models.SentryAppInstallation")
    project = Param("sentry.models.Project", required=False)
    uri = Param(six.string_types)
    query = Param(six.string_types, required=False)

    def call(self):
        return self._make_request()

    def _build_url(self):
        urlparts = list(urlparse(self.sentry_app.webhook_url))
        urlparts[2] = self.uri

        query = {"installationId": self.install.uuid}

        if self.project:
            query["projectSlug"] = self.project.slug

        if self.query:
            query["query"] = self.query

        urlparts[4] = urlencode(query)
        return urlunparse(urlparts)

    def _make_request(self):
        try:
            body = safe_urlread(
                send_and_save_sentry_app_request(
                    self._build_url(),
                    self.sentry_app,
                    self.install.organization_id,
                    "select_options.requested",
                    headers=self._build_headers(),
                ))

            response = json.loads(body)
        except Exception as e:
            logger.info(
                "select-requester.error",
                extra={
                    "sentry_app": self.sentry_app.slug,
                    "install": self.install.uuid,
                    "project": self.project and self.project.slug,
                    "uri": self.uri,
                    "error_message": six.text_type(e),
                },
            )
            response = {}

        if not self._validate_response(response):
            raise APIError()

        return self._format_response(response)

    def _validate_response(self, resp):
        return validate(instance=resp, schema_type="select")

    def _format_response(self, resp):
        # the UI expects the following form:
        # choices: [[label, value]]
        # default: [label, value]
        response = {}
        choices = []

        for option in resp:
            choices.append([option["value"], option["label"]])
            if option.get("default"):
                response["defaultValue"] = option["value"]

        response["choices"] = choices
        return response

    def _build_headers(self):
        request_uuid = uuid4().hex

        return {
            "Content-Type": "application/json",
            "Request-ID": request_uuid,
            "Sentry-App-Signature": self.sentry_app.build_signature(""),
        }

    @memoize
    def sentry_app(self):
        return self.install.sentry_app