Пример #1
0
class Project(Model):
    """
    Projects are permission based namespaces which generally
    are the top level entry point for all data.
    """
    __core__ = True

    slug = models.SlugField(null=True)
    name = models.CharField(max_length=200)
    forced_color = models.CharField(max_length=6, null=True, blank=True)
    organization = FlexibleForeignKey('sentry.Organization')
    team = FlexibleForeignKey('sentry.Team')
    teams = models.ManyToManyField('sentry.Team',
                                   related_name='teams',
                                   through=ProjectTeam)
    public = models.BooleanField(default=False)
    date_added = models.DateTimeField(default=timezone.now)
    status = BoundedPositiveIntegerField(
        default=0,
        choices=(
            (ObjectStatus.VISIBLE, _('Active')),
            (ObjectStatus.PENDING_DELETION, _('Pending Deletion')),
            (ObjectStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
        ),
        db_index=True)
    # projects that were created before this field was present
    # will have their first_event field set to date_added
    first_event = models.DateTimeField(null=True)
    flags = BitField(flags=(('has_releases',
                             'This Project has sent release data'), ),
                     default=0,
                     null=True)

    objects = ProjectManager(cache_fields=[
        'pk',
        'slug',
    ])
    platform = models.CharField(max_length=64, null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_project'
        unique_together = (('team', 'slug'), ('organization', 'slug'))

    __repr__ = sane_repr('team_id', 'name', 'slug')

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.slug)

    def next_short_id(self):
        from sentry.models import Counter
        return Counter.increment(self)

    def save(self, *args, **kwargs):
        if not self.slug:
            lock = locks.get('slug:project', duration=5)
            with TimedRetryPolicy(10)(lock.acquire):
                slugify_instance(self,
                                 self.name,
                                 organization=self.organization)
            super(Project, self).save(*args, **kwargs)
        else:
            super(Project, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return absolute_uri('/{}/{}/'.format(self.organization.slug,
                                             self.slug))

    def merge_to(self, project):
        from sentry.models import (Group, GroupTagValue, Event)

        if not isinstance(project, Project):
            project = Project.objects.get_from_cache(pk=project)

        for group in Group.objects.filter(project=self):
            try:
                other = Group.objects.get(project=project, )
            except Group.DoesNotExist:
                group.update(project=project)
                GroupTagValue.objects.filter(
                    project_id=self.id,
                    group_id=group.id,
                ).update(project_id=project.id)
            else:
                Event.objects.filter(
                    group_id=group.id, ).update(group_id=other.id)

                for obj in GroupTagValue.objects.filter(group=group):
                    obj2, created = GroupTagValue.objects.get_or_create(
                        project_id=project.id,
                        group_id=group.id,
                        key=obj.key,
                        value=obj.value,
                        defaults={'times_seen': obj.times_seen})
                    if not created:
                        obj2.update(times_seen=F('times_seen') +
                                    obj.times_seen)

        for fv in tagstore.get_tag_values(self.id):
            tagstore.get_or_create_tag_value(project_id=project.id,
                                             key=fv.key,
                                             value=fv.value)
            fv.delete()
        self.delete()

    def is_internal_project(self):
        for value in (settings.SENTRY_FRONTEND_PROJECT,
                      settings.SENTRY_PROJECT):
            if six.text_type(self.id) == six.text_type(value) or six.text_type(
                    self.slug) == six.text_type(value):
                return True
        return False

    def get_tags(self):
        from sentry import tagstore

        if not hasattr(self, '_tag_cache'):
            tags = self.get_option('tags', None)
            if tags is None:
                tags = [
                    t
                    for t in (tk.key for tk in tagstore.get_tag_keys(self.id))
                ]
            self._tag_cache = tags

        return self._tag_cache

    # TODO: Make these a mixin
    def update_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.set_value(self, *args, **kwargs)

    def get_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.get_value(self, *args, **kwargs)

    def delete_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.unset_value(self, *args, **kwargs)

    @property
    def callsign(self):
        return self.slug.upper()

    @property
    def color(self):
        if self.forced_color is not None:
            return '#%s' % self.forced_color
        return get_hashed_color(self.callsign or self.slug)

    @property
    def member_set(self):
        from sentry.models import OrganizationMember
        return self.organization.member_set.filter(
            id__in=OrganizationMember.objects.filter(
                organizationmemberteam__is_active=True,
                organizationmemberteam__team=self.team,
            ).values('id'),
            user__is_active=True,
        ).distinct()

    def has_access(self, user, access=None):
        from sentry.models import AuthIdentity, OrganizationMember

        warnings.warn('Project.has_access is deprecated.', DeprecationWarning)

        queryset = self.member_set.filter(user=user)

        if access is not None:
            queryset = queryset.filter(type__lte=access)

        try:
            member = queryset.get()
        except OrganizationMember.DoesNotExist:
            return False

        try:
            auth_identity = AuthIdentity.objects.get(
                auth_provider__organization=self.organization_id,
                user=member.user_id,
            )
        except AuthIdentity.DoesNotExist:
            return True

        return auth_identity.is_valid(member)

    def get_audit_log_data(self):
        return {
            'id': self.id,
            'slug': self.slug,
            'name': self.name,
            'status': self.status,
            'public': self.public,
        }

    def get_full_name(self):
        if self.team.name not in self.name:
            return '%s %s' % (self.team.name, self.name)
        return self.name

    def get_notification_recipients(self, user_option):
        from sentry.models import UserOption
        alert_settings = dict((o.user_id, int(o.value))
                              for o in UserOption.objects.filter(
                                  project=self,
                                  key=user_option,
                              ))

        disabled = set(u for u, v in six.iteritems(alert_settings) if v == 0)

        member_set = set(
            self.member_set.exclude(user__in=disabled, ).values_list(
                'user', flat=True))

        # determine members default settings
        members_to_check = set(u for u in member_set
                               if u not in alert_settings)
        if members_to_check:
            disabled = set((uo.user_id for uo in UserOption.objects.filter(
                key='subscribe_by_default',
                user__in=members_to_check,
            ) if uo.value == '0'))
            member_set = [x for x in member_set if x not in disabled]

        return member_set

    def get_mail_alert_subscribers(self):
        user_ids = self.get_notification_recipients('mail:alert')
        if not user_ids:
            return []
        from sentry.models import User
        return list(User.objects.filter(id__in=user_ids))

    def is_user_subscribed_to_mail_alerts(self, user):
        from sentry.models import UserOption
        is_enabled = UserOption.objects.get_value(user,
                                                  'mail:alert',
                                                  project=self)
        if is_enabled is None:
            is_enabled = UserOption.objects.get_value(user,
                                                      'subscribe_by_default',
                                                      '1') == '1'
        else:
            is_enabled = bool(is_enabled)
        return is_enabled

    def is_user_subscribed_to_workflow(self, user):
        from sentry.models import UserOption, UserOptionValue

        opt_value = UserOption.objects.get_value(user,
                                                 'workflow:notifications',
                                                 project=self)
        if opt_value is None:
            opt_value = UserOption.objects.get_value(
                user, 'workflow:notifications',
                UserOptionValue.all_conversations)
        return opt_value == UserOptionValue.all_conversations

    def transfer_to(self, team):
        from sentry.models import ReleaseProject

        organization = team.organization

        # We only need to delete ReleaseProjects when moving to a different
        # Organization. Releases are bound to Organization, so it's not realistic
        # to keep this link unless we say, copied all Releases as well.
        if self.organization_id != organization.id:
            ReleaseProject.objects.filter(project_id=self.id, ).delete()

        self.organization = organization
        self.team = team

        try:
            with transaction.atomic():
                self.update(
                    organization=organization,
                    team=team,
                )
        except IntegrityError:
            slugify_instance(self, self.name, organization=organization)
            self.update(
                slug=self.slug,
                organization=organization,
                team=team,
            )

    def add_team(self, team):
        try:
            with transaction.atomic():
                ProjectTeam.objects.create(project=self, team=team)
        except IntegrityError:
            return False
        else:
            return True
Пример #2
0
class ExportedData(Model):
    """
    Stores references to asynchronous data export jobs
    """

    __core__ = False

    organization = FlexibleForeignKey("sentry.Organization")
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                              null=True,
                              on_delete=models.SET_NULL)
    file = FlexibleForeignKey("sentry.File",
                              null=True,
                              db_constraint=False,
                              on_delete=models.SET_NULL)
    date_added = models.DateTimeField(default=timezone.now)
    date_finished = models.DateTimeField(null=True)
    date_expired = models.DateTimeField(null=True, db_index=True)
    query_type = BoundedPositiveIntegerField(
        choices=ExportQueryType.as_choices())
    query_info = JSONField()

    @property
    def status(self):
        if self.date_finished is None:
            return ExportStatus.Early
        elif self.date_expired < timezone.now():
            return ExportStatus.Expired
        else:
            return ExportStatus.Valid

    @property
    def payload(self):
        payload = self.query_info.copy()
        payload["export_type"] = ExportQueryType.as_str(self.query_type)
        return payload

    @property
    def file_name(self):
        date = self.date_added.strftime("%Y-%B-%d")
        export_type = ExportQueryType.as_str(self.query_type)
        # Example: Discover_2020-July-21_27.csv
        return "{}_{}_{}.csv".format(export_type, date, self.id)

    @staticmethod
    def format_date(date):
        # Example: 12:21 PM on July 21, 2020 (UTC)
        return None if date is None else date.strftime(
            "%-I:%M %p on %B %d, %Y (%Z)")

    def delete_file(self):
        if self.file:
            self.file.delete()

    def delete(self, *args, **kwargs):
        self.delete_file()
        super(ExportedData, self).delete(*args, **kwargs)

    def finalize_upload(self, file, expiration=DEFAULT_EXPIRATION):
        self.delete_file()  # If a file is present, remove it
        current_time = timezone.now()
        expire_time = current_time + expiration
        self.update(file=file,
                    date_finished=current_time,
                    date_expired=expire_time)
        self.email_success()

    def email_success(self):
        from sentry.utils.email import MessageBuilder

        # The following condition should never be true, but it's a safeguard in case someone manually calls this method
        if self.date_finished is None or self.date_expired is None or self.file is None:
            logger.warning(
                "Notification email attempted on incomplete dataset",
                extra={
                    "data_export_id": self.id,
                    "organization_id": self.organization_id
                },
            )
            return
        url = absolute_uri(
            reverse("sentry-data-export-details",
                    args=[self.organization.slug, self.id]))
        msg = MessageBuilder(
            subject="Your data is ready.",
            context={
                "url": url,
                "expiration": self.format_date(self.date_expired)
            },
            type="organization.export-data",
            template="sentry/emails/data-export-success.txt",
            html_template="sentry/emails/data-export-success.html",
        )
        msg.send_async([self.user.email])
        metrics.incr("dataexport.end", instance="success")

    def email_failure(self, message):
        from sentry.utils.email import MessageBuilder

        msg = MessageBuilder(
            subject="We couldn't export your data.",
            context={
                "creation": self.format_date(self.date_added),
                "error_message": message,
                "payload": json.dumps(self.payload, indent=2, sort_keys=True),
            },
            type="organization.export-data",
            template="sentry/emails/data-export-failure.txt",
            html_template="sentry/emails/data-export-failure.html",
        )
        msg.send_async([self.user.email])
        metrics.incr("dataexport.end", instance="failure")
        self.delete()

    class Meta:
        app_label = "sentry"
        db_table = "sentry_exporteddata"

    __repr__ = sane_repr("query_type", "query_info")
Пример #3
0
class OrganizationMember(Model):
    """
    Identifies relationships between teams and users.

    Users listed as team members are considered to have access to all projects
    and could be thought of as team owners (though their access level may not)
    be set to ownership.
    """
    organization = FlexibleForeignKey('sentry.Organization',
                                      related_name="member_set")

    user = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                              null=True,
                              blank=True,
                              related_name="sentry_orgmember_set")
    email = models.EmailField(null=True, blank=True)
    role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )
    flags = BitField(flags=(
        ('sso:linked', 'sso:linked'),
        ('sso:invalid', 'sso:invalid'),
    ),
                     default=0)
    date_added = models.DateTimeField(default=timezone.now)
    has_global_access = models.BooleanField(default=True)
    counter = BoundedPositiveIntegerField(null=True, blank=True)
    teams = models.ManyToManyField('sentry.Team',
                                   blank=True,
                                   through='sentry.OrganizationMemberTeam')

    # Deprecated -- no longer used
    type = BoundedPositiveIntegerField(default=50, blank=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_organizationmember'
        unique_together = (
            ('organization', 'user'),
            ('organization', 'email'),
        )

    __repr__ = sane_repr(
        'organization_id',
        'user_id',
        'role',
    )

    @transaction.atomic
    def save(self, *args, **kwargs):
        assert self.user_id or self.email, \
            'Must set user or email'
        super(OrganizationMember, self).save(*args, **kwargs)

        if not self.counter:
            self._set_counter()

    @transaction.atomic
    def delete(self, *args, **kwargs):
        super(OrganizationMember, self).delete(*args, **kwargs)
        if self.counter:
            self._unshift_counter()

    def _unshift_counter(self):
        assert self.counter
        OrganizationMember.objects.filter(
            organization=self.organization,
            counter__gt=self.counter,
        ).update(counter=F('counter') - 1, )

    def _set_counter(self):
        assert self.id and not self.counter
        # XXX(dcramer): this isnt atomic, but unfortunately MySQL doesnt support
        # the subquery pattern we'd need
        self.update(counter=OrganizationMember.objects.filter(
            organization=self.organization, ).count(), )

    @property
    def is_pending(self):
        return self.user_id is None

    @property
    def token(self):
        checksum = md5()
        for x in (str(self.organization_id), self.get_email(),
                  settings.SECRET_KEY):
            checksum.update(x)
        return checksum.hexdigest()

    def send_invite_email(self):
        from sentry.utils.email import MessageBuilder

        context = {
            'email':
            self.email,
            'organization':
            self.organization,
            'url':
            absolute_uri(
                reverse('sentry-accept-invite',
                        kwargs={
                            'member_id': self.id,
                            'token': self.token,
                        })),
        }

        msg = MessageBuilder(
            subject='Join %s in using Sentry' % self.organization.name,
            template='sentry/emails/member-invite.txt',
            html_template='sentry/emails/member-invite.html',
            context=context,
        )

        try:
            msg.send([self.get_email()])
        except Exception as e:
            logger = logging.getLogger('sentry.mail.errors')
            logger.exception(e)

    def send_sso_link_email(self):
        from sentry.utils.email import MessageBuilder

        context = {
            'email':
            self.email,
            'organization_name':
            self.organization.name,
            'url':
            absolute_uri(
                reverse('sentry-auth-organization',
                        kwargs={
                            'organization_slug': self.organization.slug,
                        })),
        }

        msg = MessageBuilder(
            subject='Action Required for %s' % (self.organization.name, ),
            template='sentry/emails/auth-link-identity.txt',
            html_template='sentry/emails/auth-link-identity.html',
            context=context,
        )
        msg.send_async([self.get_email()])

    def get_display_name(self):
        if self.user_id:
            return self.user.get_display_name()
        return self.email

    def get_label(self):
        if self.user_id:
            return self.user.get_label()
        return self.email or self.id

    def get_email(self):
        if self.user_id:
            return self.user.email
        return self.email

    def get_audit_log_data(self):
        from sentry.models import Team
        return {
            'email':
            self.email,
            'user':
            self.user_id,
            'teams':
            list(
                Team.objects.filter(
                    id__in=OrganizationMemberTeam.objects.filter(
                        organizationmember=self,
                        is_active=True,
                    ).values_list('team', flat=True))),
            'has_global_access':
            self.has_global_access,
            'role':
            self.role,
        }

    def get_teams(self):
        from sentry.models import Team

        if roles.get(self.role).is_global:
            return self.organization.team_set.all()

        return Team.objects.filter(
            id__in=OrganizationMemberTeam.objects.filter(
                organizationmember=self,
                is_active=True,
            ).values('team'))

    def get_scopes(self):
        return roles.get(self.role).scopes

    def can_manage_member(self, member):
        return roles.can_manage(self.role, member.role)
Пример #4
0
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project', null=True)
    logger = models.CharField(max_length=64,
                              blank=True,
                              default=DEFAULT_LOGGER_NAME,
                              db_index=True)
    level = BoundedPositiveIntegerField(choices=LOG_LEVELS.items(),
                                        default=logging.ERROR,
                                        blank=True,
                                        db_index=True)
    message = models.TextField()
    culprit = models.CharField(max_length=MAX_CULPRIT_LENGTH,
                               blank=True,
                               null=True,
                               db_column='view')
    num_comments = BoundedPositiveIntegerField(default=0, null=True)
    platform = models.CharField(max_length=64, null=True)
    status = BoundedPositiveIntegerField(default=0,
                                         choices=(
                                             (GroupStatus.UNRESOLVED,
                                              _('Unresolved')),
                                             (GroupStatus.RESOLVED,
                                              _('Resolved')),
                                             (GroupStatus.MUTED, _('Muted')),
                                         ),
                                         db_index=True)
    times_seen = BoundedPositiveIntegerField(default=1, db_index=True)
    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_release = FlexibleForeignKey('sentry.Release', null=True)
    resolved_at = models.DateTimeField(null=True, db_index=True)
    # active_at should be the same as first_seen by default
    active_at = models.DateTimeField(null=True, db_index=True)
    time_spent_total = BoundedIntegerField(default=0)
    time_spent_count = BoundedIntegerField(default=0)
    score = BoundedIntegerField(default=0)
    is_public = models.NullBooleanField(default=False, null=True)
    data = GzippedDictField(blank=True, null=True)

    objects = GroupManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_groupedmessage'
        verbose_name_plural = _('grouped messages')
        verbose_name = _('grouped message')
        permissions = (("can_view", "Can view"), )
        index_together = (('project', 'first_release'), )

    __repr__ = sane_repr('project_id')

    def __unicode__(self):
        return "(%s) %s" % (self.times_seen, self.error())

    def save(self, *args, **kwargs):
        if not self.last_seen:
            self.last_seen = timezone.now()
        if not self.first_seen:
            self.first_seen = self.last_seen
        if not self.active_at:
            self.active_at = self.first_seen
        if self.message:
            # We limit what we store for the message body
            self.message = self.message.splitlines()[0][:255]
        super(Group, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return absolute_uri(
            reverse('sentry-group',
                    args=[self.organization.slug, self.project.slug, self.id]))

    @property
    def avg_time_spent(self):
        if not self.time_spent_count:
            return
        return float(self.time_spent_total) / self.time_spent_count

    def is_over_resolve_age(self):
        resolve_age = self.project.get_option('sentry:resolve_age', None)
        if not resolve_age:
            return False
        return self.last_seen < timezone.now() - timedelta(
            hours=int(resolve_age))

    def is_muted(self):
        return self.get_status() == GroupStatus.MUTED

    def is_resolved(self):
        return self.get_status() == GroupStatus.RESOLVED

    def get_status(self):
        if self.status == GroupStatus.UNRESOLVED and self.is_over_resolve_age(
        ):
            return GroupStatus.RESOLVED
        return self.status

    def get_share_id(self):
        return b16encode('{}.{}'.format(self.project_id, self.id)).lower()

    @classmethod
    def from_share_id(cls, share_id):
        try:
            project_id, group_id = b16decode(share_id.upper()).split('.')
        except ValueError:
            raise cls.DoesNotExist
        return cls.objects.get(project=project_id, id=group_id)

    def get_score(self):
        return int(
            math.log(self.times_seen) * 600 +
            float(time.mktime(self.last_seen.timetuple())))

    def get_latest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_latest_event'):
            try:
                self._latest_event = Event.objects.filter(
                    group=self, ).order_by('-datetime')[0]
            except IndexError:
                self._latest_event = None
        return self._latest_event

    def get_unique_tags(self, tag, since=None, order_by='-times_seen'):
        # TODO(dcramer): this has zero test coverage and is a critical path
        from sentry.models import GroupTagValue

        queryset = GroupTagValue.objects.filter(
            group=self,
            key=tag,
        )
        if since:
            queryset = queryset.filter(last_seen__gte=since)
        return queryset.values_list(
            'value',
            'times_seen',
            'first_seen',
            'last_seen',
        ).order_by(order_by)

    def get_tags(self, with_internal=True):
        from sentry.models import GroupTagKey, TagKey
        if not hasattr(self, '_tag_cache'):
            group_tags = GroupTagKey.objects.filter(
                group=self,
                project=self.project,
            )
            if not with_internal:
                group_tags = group_tags.exclude(key__startswith='sentry:')

            group_tags = list(group_tags.values_list('key', flat=True))

            tag_keys = dict(
                (t.key, t) for t in TagKey.objects.filter(project=self.project,
                                                          key__in=group_tags))

            results = []
            for key in group_tags:
                try:
                    tag_key = tag_keys[key]
                except KeyError:
                    label = key.replace('_', ' ').title()
                else:
                    label = tag_key.get_label()

                results.append({
                    'key': key,
                    'label': label,
                })

            self._tag_cache = sorted(results, key=lambda x: x['label'])

        return self._tag_cache

    def error(self):
        return self.message

    error.short_description = _('error')

    def has_two_part_message(self):
        message = strip(self.message)
        return '\n' in message or len(message) > 100

    @property
    def title(self):
        culprit = strip(self.culprit)
        if culprit:
            return culprit
        return self.message

    @property
    def message_short(self):
        message = strip(self.message)
        if not message:
            message = '<unlabeled message>'
        else:
            message = truncatechars(message.splitlines()[0], 100)
        return message

    @property
    def organization(self):
        return self.project.organization

    @property
    def team(self):
        return self.project.team

    @property
    def checksum(self):
        warnings.warn('Group.checksum is no longer used', DeprecationWarning)
        return ''

    def get_email_subject(self):
        return '[%s %s] %s: %s' % (
            self.team.name.encode('utf-8'), self.project.name.encode('utf-8'),
            six.text_type(self.get_level_display()).upper().encode('utf-8'),
            self.message_short.encode('utf-8'))
Пример #5
0
class GroupResolution(Model):
    """
    Describes when a group was marked as resolved.
    """

    __core__ = False

    class Type:
        in_release = 0
        in_next_release = 1

    class Status:
        pending = 0
        resolved = 1

    group = FlexibleForeignKey("sentry.Group", unique=True)
    # the release in which its suggested this was resolved
    # which allows us to indicate if it still happens in newer versions
    release = FlexibleForeignKey("sentry.Release")
    type = BoundedPositiveIntegerField(
        choices=((Type.in_next_release, "in_next_release"), (Type.in_release,
                                                             "in_release")),
        null=True,
    )
    actor_id = BoundedPositiveIntegerField(null=True)
    datetime = models.DateTimeField(default=timezone.now, db_index=True)
    status = BoundedPositiveIntegerField(
        default=Status.pending,
        choices=((Status.pending, _("Pending")), (Status.resolved,
                                                  _("Resolved"))),
    )

    class Meta:
        db_table = "sentry_groupresolution"
        app_label = "sentry"

    __repr__ = sane_repr("group_id", "release_id")

    @classmethod
    def has_resolution(cls, group, release):
        """
        Determine if a resolution exists for the given group and release.

        This is used to suggest if a regression has occurred.
        """
        try:
            res_type, res_release, res_release_datetime = (cls.objects.filter(
                group=group).select_related("release").values_list(
                    "type", "release__id", "release__date_added")[0])
        except IndexError:
            return False

        # if no release is present, we assume we've gone from "no release" to "some release"
        # in application configuration, and thus this must be older
        if not release:
            return True

        if res_type in (None, cls.Type.in_next_release):
            if res_release == release.id:
                return True
            elif res_release_datetime > release.date_added:
                return True
            return False
        elif res_type == cls.Type.in_release:
            if res_release == release.id:
                return False
            if res_release_datetime < release.date_added:
                return False
            return True
        else:
            raise NotImplementedError
Пример #6
0
class ProjectKey(Model):
    __core__ = True

    project = FlexibleForeignKey('sentry.Project', related_name='key_set')
    label = models.CharField(max_length=64, blank=True, null=True)
    public_key = models.CharField(max_length=32, unique=True, null=True)
    secret_key = models.CharField(max_length=32, unique=True, null=True)
    roles = BitField(flags=(
        # access to post events to the store endpoint
        ('store', 'Event API access'),

        # read/write access to rest API
        ('api', 'Web API access'),
    ), default=['store'])
    status = BoundedPositiveIntegerField(default=0, choices=(
        (ProjectKeyStatus.ACTIVE, _('Active')),
        (ProjectKeyStatus.INACTIVE, _('Inactive')),
    ), db_index=True)
    date_added = models.DateTimeField(default=timezone.now, null=True)

    objects = BaseManager(cache_fields=(
        'public_key',
        'secret_key',
    ))

    # support legacy project keys in API
    scopes = (
        'project:read',
        'project:write',
        'project:admin',
        'project:releases',
        'event:read',
        'event:write',
        'event:admin',
    )

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_projectkey'

    __repr__ = sane_repr('project_id', 'public_key')

    def __unicode__(self):
        return six.text_type(self.public_key)

    @classmethod
    def generate_api_key(cls):
        return uuid4().hex

    @classmethod
    def looks_like_api_key(cls, key):
        return bool(_uuid4_re.match(key))

    @classmethod
    def from_dsn(cls, dsn):
        urlparts = urlparse(dsn)

        public_key = urlparts.username
        project_id = urlparts.path.rsplit('/', 1)[-1]

        try:
            return ProjectKey.objects.get(
                public_key=public_key,
                project=project_id,
            )
        except ValueError:
            # ValueError would come from a non-integer project_id,
            # which is obviously a DoesNotExist. We catch and rethrow this
            # so anything downstream expecting DoesNotExist works fine
            raise ProjectKey.DoesNotExist('ProjectKey matching query does not exist.')

    @classmethod
    def get_default(cls, project):
        try:
            return cls.objects.filter(
                project=project,
                roles=cls.roles.store,
                status=ProjectKeyStatus.ACTIVE
            )[0]
        except IndexError:
            return None

    @property
    def is_active(self):
        return self.status == ProjectKeyStatus.ACTIVE

    def save(self, *args, **kwargs):
        if not self.public_key:
            self.public_key = ProjectKey.generate_api_key()
        if not self.secret_key:
            self.secret_key = ProjectKey.generate_api_key()
        if not self.label:
            self.label = petname.Generate(2, ' ', letters=10).title()
        super(ProjectKey, self).save(*args, **kwargs)

    def get_dsn(self, domain=None, secure=True, public=False):
        if not public:
            key = '%s:%s' % (self.public_key, self.secret_key)
            url = settings.SENTRY_ENDPOINT
        else:
            key = self.public_key
            url = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT

        if url:
            urlparts = urlparse(url)
        else:
            urlparts = urlparse(options.get('system.url-prefix'))

        return '%s://%s@%s/%s' % (
            urlparts.scheme,
            key,
            urlparts.netloc + urlparts.path,
            self.project_id,
        )

    @property
    def dsn_private(self):
        return self.get_dsn(public=False)

    @property
    def dsn_public(self):
        return self.get_dsn(public=True)

    @property
    def csp_endpoint(self):
        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
        if not endpoint:
            endpoint = options.get('system.url-prefix')

        return '%s%s?sentry_key=%s' % (
            endpoint,
            reverse('sentry-api-csp-report', args=[self.project_id]),
            self.public_key,
        )

    def get_allowed_origins(self):
        from sentry.utils.http import get_origins
        return get_origins(self.project)

    def get_audit_log_data(self):
        return {
            'label': self.label,
            'public_key': self.public_key,
            'secret_key': self.secret_key,
            'roles': int(self.roles),
            'status': self.status,
        }

    def get_scopes(self):
        return self.scopes
Пример #7
0
class TagKey(Model):
    """
    Stores references to available filters keys.
    """
    __core__ = False

    project_id = BoundedBigIntegerField(db_index=True)
    environment_id = BoundedBigIntegerField()
    key = models.CharField(max_length=MAX_TAG_KEY_LENGTH)
    values_seen = BoundedPositiveIntegerField(default=0)
    status = BoundedPositiveIntegerField(choices=(
        (TagKeyStatus.VISIBLE, _('Visible')),
        (TagKeyStatus.PENDING_DELETION, _('Pending Deletion')),
        (TagKeyStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
    ),
                                         default=TagKeyStatus.VISIBLE)

    objects = TagStoreManager()

    class Meta:
        app_label = 'tagstore'
        unique_together = (('project_id', 'environment_id', 'key'), )

    __repr__ = sane_repr('project_id', 'environment_id', 'key')

    def delete(self):
        using = router.db_for_read(TagKey)
        cursor = connections[using].cursor()
        cursor.execute(
            """
            DELETE FROM tagstore_tagkey
            WHERE project_id = %s
              AND id = %s
        """, [self.project_id, self.id])

    def get_label(self):
        from sentry import tagstore

        return tagstore.get_tag_key_label(self.key)

    def get_audit_log_data(self):
        return {
            'key': self.key,
        }

    @classmethod
    def get_cache_key(cls, project_id, environment_id, key):
        return 'tagkey:1:%s:%s:%s' % (project_id, environment_id,
                                      md5_text(key).hexdigest())

    @classmethod
    def get_or_create(cls, project_id, environment_id, key, **kwargs):
        cache_key = cls.get_cache_key(project_id, environment_id, key)

        rv = cache.get(cache_key)
        created = False
        if rv is None:
            rv, created = cls.objects.get_or_create(
                project_id=project_id,
                environment_id=environment_id,
                key=key,
                **kwargs)
            cache.set(cache_key, rv, 3600)

        return rv, created
Пример #8
0
class AuditLogEntry(Model):
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    actor_label = models.CharField(max_length=64, null=True, blank=True)
    # if the entry was created via a user
    actor = FlexibleForeignKey('sentry.User',
                               related_name='audit_actors',
                               null=True,
                               blank=True)
    # if the entry was created via an api key
    actor_key = FlexibleForeignKey('sentry.ApiKey', null=True, blank=True)
    target_object = BoundedPositiveIntegerField(null=True)
    target_user = FlexibleForeignKey('sentry.User',
                                     null=True,
                                     blank=True,
                                     related_name='audit_targets')
    # TODO(dcramer): we want to compile this mapping into JSX for the UI
    event = BoundedPositiveIntegerField(choices=(
        # We emulate github a bit with event naming
        (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'),
        (AuditLogEntryEvent.MEMBER_ADD, 'member.add'),
        (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'),
        (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'),
        (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'),
        (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'),
        (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'),
        (AuditLogEntryEvent.MEMBER_PENDING, 'member.pending'),
        (AuditLogEntryEvent.TEAM_ADD, 'team.create'),
        (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'),
        (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'),
        (AuditLogEntryEvent.PROJECT_ADD, 'project.create'),
        (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'),
        (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'),
        (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'),
        (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'),
        (AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER,
         'project.request-transfer'),
        (AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER,
         'project.accept-transfer'),
        (AuditLogEntryEvent.PROJECT_ENABLE, 'project.enable'),
        (AuditLogEntryEvent.PROJECT_DISABLE, 'project.disable'),
        (AuditLogEntryEvent.ORG_ADD, 'org.create'),
        (AuditLogEntryEvent.ORG_EDIT, 'org.edit'),
        (AuditLogEntryEvent.ORG_REMOVE, 'org.remove'),
        (AuditLogEntryEvent.ORG_RESTORE, 'org.restore'),
        (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'),
        (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'),
        (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'),
        (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'),
        (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'),
        (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'),
        (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'),
        (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'),
        (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'),
        (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'),
        (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'),
        (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'),
        (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'),
        (AuditLogEntryEvent.RULE_ADD, 'rule.create'),
        (AuditLogEntryEvent.RULE_EDIT, 'rule.edit'),
        (AuditLogEntryEvent.RULE_REMOVE, 'rule.remove'),
        (AuditLogEntryEvent.SERVICEHOOK_ADD, 'serivcehook.create'),
        (AuditLogEntryEvent.SERVICEHOOK_EDIT, 'serivcehook.edit'),
        (AuditLogEntryEvent.SERVICEHOOK_REMOVE, 'serivcehook.remove'),
        (AuditLogEntryEvent.SERVICEHOOK_ENABLE, 'serivcehook.enable'),
        (AuditLogEntryEvent.SERVICEHOOK_DISABLE, 'serivcehook.disable'),
        (AuditLogEntryEvent.INTEGRATION_ADD, 'integration.add'),
        (AuditLogEntryEvent.INTEGRATION_EDIT, 'integration.edit'),
        (AuditLogEntryEvent.INTEGRATION_REMOVE, 'integration.remove'),
        (AuditLogEntryEvent.SENTRY_APP_ADD, 'sentry-app.add'),
        (AuditLogEntryEvent.SENTRY_APP_REMOVE, 'sentry-app.remove'),
        (AuditLogEntryEvent.SENTRY_APP_INSTALL, 'sentry-app.install'),
        (AuditLogEntryEvent.SENTRY_APP_UNINSTALL, 'sentry-app.uninstall'),
        (AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD,
         'internal-integration.create'),
        (AuditLogEntryEvent.SET_ONDEMAND, 'ondemand.edit'),
        (AuditLogEntryEvent.TRIAL_STARTED, 'trial.started'),
        (AuditLogEntryEvent.PLAN_CHANGED, 'plan.changed'),
        (AuditLogEntryEvent.PLAN_CANCELLED, 'plan.cancelled'),
    ))
    ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_auditlogentry'

    __repr__ = sane_repr('organization_id', 'type')

    def save(self, *args, **kwargs):
        if not self.actor_label:
            assert self.actor or self.actor_key
            if self.actor:
                self.actor_label = self.actor.username
            else:
                self.actor_label = self.actor_key.key
        super(AuditLogEntry, self).save(*args, **kwargs)

    def get_actor_name(self):
        if self.actor:
            return self.actor.get_display_name()
        elif self.actor_key:
            return self.actor_key.key + ' (api key)'
        return self.actor_label

    def get_note(self):
        if self.event == AuditLogEntryEvent.MEMBER_INVITE:
            return 'invited member %s' % (self.data['email'], )
        elif self.event == AuditLogEntryEvent.MEMBER_ADD:
            if self.target_user == self.actor:
                return 'joined the organization'
            return 'added member %s' % (self.target_user.get_display_name(), )
        elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT:
            return 'accepted the membership invite'
        elif self.event == AuditLogEntryEvent.MEMBER_REMOVE:
            if self.target_user == self.actor:
                return 'left the organization'
            return 'removed member %s' % (
                self.data.get('email')
                or self.target_user.get_display_name(), )
        elif self.event == AuditLogEntryEvent.MEMBER_EDIT:
            return 'edited member %s (role: %s, teams: %s)' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data.get('role') or 'N/A',
                ', '.join(
                    six.text_type(x)
                    for x in self.data.get('team_slugs', [])) or 'N/A',
            )
        elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM:
            if self.target_user == self.actor:
                return 'joined team %s' % (self.data['team_slug'], )
            return 'added %s to team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )
        elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM:
            if self.target_user == self.actor:
                return 'left team %s' % (self.data['team_slug'], )
            return 'removed %s from team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )
        elif self.event == AuditLogEntryEvent.MEMBER_PENDING:
            return 'required member %s to setup 2FA' % (
                self.data.get('email')
                or self.target_user.get_display_name(), )

        elif self.event == AuditLogEntryEvent.ORG_ADD:
            return 'created the organization'
        elif self.event == AuditLogEntryEvent.ORG_EDIT:
            return 'edited the organization setting: ' + (', '.join(
                u'{} {}'.format(k, v) for k, v in self.data.items()))
        elif self.event == AuditLogEntryEvent.ORG_REMOVE:
            return 'removed the organization'
        elif self.event == AuditLogEntryEvent.ORG_RESTORE:
            return 'restored the organization'

        elif self.event == AuditLogEntryEvent.TEAM_ADD:
            return 'created team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_EDIT:
            return 'edited team %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.TEAM_REMOVE:
            return 'removed team %s' % (self.data['slug'], )

        elif self.event == AuditLogEntryEvent.PROJECT_ADD:
            return 'created project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_EDIT:
            return 'edited project settings ' + (' '.join([
                ' in %s to %s' % (key, value)
                for (key, value) in six.iteritems(self.data)
            ]))
        elif self.event == AuditLogEntryEvent.PROJECT_REMOVE:
            return 'removed project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_REQUEST_TRANSFER:
            return 'requested to transfer project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_ACCEPT_TRANSFER:
            return 'accepted transfer of project %s' % (self.data['slug'], )
        elif self.event == AuditLogEntryEvent.PROJECT_ENABLE:
            if isinstance(self.data['state'], set):
                return 'enabled project filter %s' % (self.data['state'], )
            return 'enabled project filter %s' % (', '.join(
                self.data["state"]), )
        elif self.event == AuditLogEntryEvent.PROJECT_DISABLE:
            if isinstance(self.data['state'], set):
                return 'disabled project filter %s' % (self.data['state'], )
            return 'disabled project filter %s' % (', '.join(
                self.data["state"]), )

        elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE:
            return 'removed tags matching %s = *' % (self.data['key'], )

        elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD:
            return 'added project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT:
            return 'edited project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE:
            return 'removed project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE:
            return 'enabled project key %s' % (self.data['public_key'], )
        elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE:
            return 'disabled project key %s' % (self.data['public_key'], )

        elif self.event == AuditLogEntryEvent.SSO_ENABLE:
            return 'enabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_DISABLE:
            return 'disabled sso (%s)' % (self.data['provider'], )
        elif self.event == AuditLogEntryEvent.SSO_EDIT:
            return 'edited sso settings: ' + (', '.join(
                u'{} {}'.format(k, v) for k, v in self.data.items()))
        elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK:
            return 'linked their account to a new identity'

        elif self.event == AuditLogEntryEvent.APIKEY_ADD:
            return 'added api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_EDIT:
            return 'edited api key %s' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.APIKEY_REMOVE:
            return 'removed api key %s' % (self.data['label'], )

        elif self.event == AuditLogEntryEvent.RULE_ADD:
            return 'added rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_EDIT:
            return 'edited rule "%s"' % (self.data['label'], )
        elif self.event == AuditLogEntryEvent.RULE_REMOVE:
            return 'removed rule "%s"' % (self.data['label'], )

        elif self.event == AuditLogEntryEvent.SET_ONDEMAND:
            if self.data['ondemand'] == -1:
                return 'changed on-demand spend to unlimited'
            return 'changed on-demand max spend to $%d' % (
                self.data['ondemand'] / 100, )
        elif self.event == AuditLogEntryEvent.TRIAL_STARTED:
            return 'started trial'
        elif self.event == AuditLogEntryEvent.PLAN_CHANGED:
            return 'changed plan to %s' % (self.data['plan_name'], )
        elif self.event == AuditLogEntryEvent.PLAN_CANCELLED:
            return 'cancelled plan'

        elif self.event == AuditLogEntryEvent.SERVICEHOOK_ADD:
            return 'added a service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_EDIT:
            return 'edited the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_REMOVE:
            return 'removed the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_ENABLE:
            return 'enabled theservice hook for "%s"' % (truncatechars(
                self.data['url'], 64), )
        elif self.event == AuditLogEntryEvent.SERVICEHOOK_DISABLE:
            return 'disabled the service hook for "%s"' % (truncatechars(
                self.data['url'], 64), )

        elif self.event == AuditLogEntryEvent.INTEGRATION_ADD:
            return 'enabled integration %s for project %s' % (
                self.data['integration'], self.data['project'])
        elif self.event == AuditLogEntryEvent.INTEGRATION_EDIT:
            return 'edited integration %s for project %s' % (
                self.data['integration'], self.data['project'])
        elif self.event == AuditLogEntryEvent.INTEGRATION_REMOVE:
            return 'disabled integration %s from project %s' % (
                self.data['integration'], self.data['project'])

        elif self.event == AuditLogEntryEvent.SENTRY_APP_ADD:
            return 'created sentry app %s' % (self.data['sentry_app'])
        elif self.event == AuditLogEntryEvent.SENTRY_APP_REMOVE:
            return 'removed sentry app %s' % (self.data['sentry_app'])
        elif self.event == AuditLogEntryEvent.SENTRY_APP_INSTALL:
            return 'installed sentry app %s' % (self.data['sentry_app'])
        elif self.event == AuditLogEntryEvent.SENTRY_APP_UNINSTALL:
            return 'uninstalled sentry app %s' % (self.data['sentry_app'])

        return ''
Пример #9
0
class Project(Model):
    """
    Projects are permission based namespaces which generally
    are the top level entry point for all data.
    """
    slug = models.SlugField(null=True)
    name = models.CharField(max_length=200)
    forced_color = models.CharField(max_length=6, null=True)
    organization = FlexibleForeignKey('sentry.Organization')
    team = FlexibleForeignKey('sentry.Team')
    public = models.BooleanField(default=False)
    date_added = models.DateTimeField(default=timezone.now)
    status = BoundedPositiveIntegerField(
        default=0,
        choices=(
            (ProjectStatus.VISIBLE, _('Active')),
            (ProjectStatus.PENDING_DELETION, _('Pending Deletion')),
            (ProjectStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
        ),
        db_index=True)
    # projects that were created before this field was present
    # will have their first_event field set to date_added
    first_event = models.DateTimeField(null=True)

    objects = ProjectManager(cache_fields=[
        'pk',
        'slug',
    ])

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_project'
        unique_together = (('team', 'slug'), ('organization', 'slug'))

    __repr__ = sane_repr('team_id', 'slug')

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.slug)

    def next_short_id(self):
        from sentry.models import Counter
        return Counter.increment(self)

    def save(self, *args, **kwargs):
        if not self.slug:
            lock_key = 'slug:project'
            with Lock(lock_key):
                slugify_instance(self,
                                 self.name,
                                 organization=self.organization)
            super(Project, self).save(*args, **kwargs)
        else:
            super(Project, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return absolute_uri(
            reverse('sentry-stream', args=[self.organization.slug, self.slug]))

    def merge_to(self, project):
        from sentry.models import (Group, GroupTagValue, Event, TagValue)

        if not isinstance(project, Project):
            project = Project.objects.get_from_cache(pk=project)

        for group in Group.objects.filter(project=self):
            try:
                other = Group.objects.get(project=project, )
            except Group.DoesNotExist:
                group.update(project=project)
                GroupTagValue.objects.filter(
                    project=self,
                    group_id=group,
                ).update(project=project)
            else:
                Event.objects.filter(
                    group_id=group.id, ).update(group_id=other.id)

                for obj in GroupTagValue.objects.filter(group=group):
                    obj2, created = GroupTagValue.objects.get_or_create(
                        project=project,
                        group=group,
                        key=obj.key,
                        value=obj.value,
                        defaults={'times_seen': obj.times_seen})
                    if not created:
                        obj2.update(times_seen=F('times_seen') +
                                    obj.times_seen)

        for fv in TagValue.objects.filter(project=self):
            TagValue.objects.get_or_create(project=project,
                                           key=fv.key,
                                           value=fv.value)
            fv.delete()
        self.delete()

    def is_internal_project(self):
        for value in (settings.SENTRY_FRONTEND_PROJECT,
                      settings.SENTRY_PROJECT):
            if str(self.id) == str(value) or str(self.slug) == str(value):
                return True
        return False

    def get_tags(self, with_internal=True):
        from sentry.models import TagKey

        if not hasattr(self, '_tag_cache'):
            tags = self.get_option('tags', None)
            if tags is None:
                tags = [
                    t for t in TagKey.objects.all_keys(self)
                    if with_internal or not t.startswith('sentry:')
                ]
            self._tag_cache = tags
        return self._tag_cache

    # TODO: Make these a mixin
    def update_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.set_value(self, *args, **kwargs)

    def get_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.get_value(self, *args, **kwargs)

    def delete_option(self, *args, **kwargs):
        from sentry.models import ProjectOption

        return ProjectOption.objects.unset_value(self, *args, **kwargs)

    @property
    def callsign(self):
        return self.slug.upper()

    @property
    def color(self):
        if self.forced_color is not None:
            return '#%s' % self.forced_color
        return get_hashed_color(self.callsign or self.slug)

    @property
    def member_set(self):
        from sentry.models import OrganizationMember
        return self.organization.member_set.filter(
            id__in=OrganizationMember.objects.filter(
                organizationmemberteam__is_active=True,
                organizationmemberteam__team=self.team,
            ).values('id'),
            user__is_active=True,
        ).distinct()

    def has_access(self, user, access=None):
        from sentry.models import AuthIdentity, OrganizationMember

        warnings.warn('Project.has_access is deprecated.', DeprecationWarning)

        queryset = self.member_set.filter(user=user)

        if access is not None:
            queryset = queryset.filter(type__lte=access)

        try:
            member = queryset.get()
        except OrganizationMember.DoesNotExist:
            return False

        try:
            auth_identity = AuthIdentity.objects.get(
                auth_provider__organization=self.organization_id,
                user=member.user_id,
            )
        except AuthIdentity.DoesNotExist:
            return True

        return auth_identity.is_valid(member)

    def get_audit_log_data(self):
        return {
            'id': self.id,
            'slug': self.slug,
            'name': self.name,
            'status': self.status,
            'public': self.public,
        }

    def get_full_name(self):
        if self.team.name not in self.name:
            return '%s %s' % (self.team.name, self.name)
        return self.name
Пример #10
0
class Activity(Model):
    __core__ = False

    SET_RESOLVED = 1
    SET_UNRESOLVED = 2
    SET_IGNORED = 3
    SET_PUBLIC = 4
    SET_PRIVATE = 5
    SET_REGRESSION = 6
    CREATE_ISSUE = 7
    NOTE = 8
    FIRST_SEEN = 9
    RELEASE = 10
    ASSIGNED = 11
    UNASSIGNED = 12
    SET_RESOLVED_IN_RELEASE = 13
    MERGE = 14
    SET_RESOLVED_BY_AGE = 15
    SET_RESOLVED_IN_COMMIT = 16
    DEPLOY = 17
    NEW_PROCESSING_ISSUES = 18
    UNMERGE_SOURCE = 19
    UNMERGE_DESTINATION = 20
    SET_RESOLVED_IN_PULL_REQUEST = 21
    REPROCESS = 22

    TYPE = (
        # (TYPE, verb-slug)
        (SET_RESOLVED, u"set_resolved"),
        (SET_RESOLVED_BY_AGE, u"set_resolved_by_age"),
        (SET_RESOLVED_IN_RELEASE, u"set_resolved_in_release"),
        (SET_RESOLVED_IN_COMMIT, u"set_resolved_in_commit"),
        (SET_RESOLVED_IN_PULL_REQUEST, u"set_resolved_in_pull_request"),
        (SET_UNRESOLVED, u"set_unresolved"),
        (SET_IGNORED, u"set_ignored"),
        (SET_PUBLIC, u"set_public"),
        (SET_PRIVATE, u"set_private"),
        (SET_REGRESSION, u"set_regression"),
        (CREATE_ISSUE, u"create_issue"),
        (NOTE, u"note"),
        (FIRST_SEEN, u"first_seen"),
        (RELEASE, u"release"),
        (ASSIGNED, u"assigned"),
        (UNASSIGNED, u"unassigned"),
        (MERGE, u"merge"),
        (DEPLOY, u"deploy"),
        (NEW_PROCESSING_ISSUES, u"new_processing_issues"),
        (UNMERGE_SOURCE, u"unmerge_source"),
        (UNMERGE_DESTINATION, u"unmerge_destination"),
        # The user has reprocessed the group, so events may have moved to new groups
        (REPROCESS, u"reprocess"),
    )

    project = FlexibleForeignKey("sentry.Project")
    group = FlexibleForeignKey("sentry.Group", null=True)
    # index on (type, ident)
    type = BoundedPositiveIntegerField(choices=TYPE)
    ident = models.CharField(max_length=64, null=True)
    # if the user is not set, it's assumed to be the system
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                              null=True,
                              on_delete=models.SET_NULL)
    datetime = models.DateTimeField(default=timezone.now)
    data = GzippedDictField(null=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_activity"

    __repr__ = sane_repr("project_id", "group_id", "event_id", "user_id",
                         "type", "ident")

    @staticmethod
    def get_version_ident(version):
        return (version or "")[:64]

    def __init__(self, *args, **kwargs):
        super(Activity, self).__init__(*args, **kwargs)
        from sentry.models import Release

        # XXX(dcramer): fix for bad data
        if self.type in (self.RELEASE, self.DEPLOY) and isinstance(
                self.data["version"], Release):
            self.data["version"] = self.data["version"].version
        if self.type == self.ASSIGNED:
            self.data["assignee"] = six.text_type(self.data["assignee"])

    def save(self, *args, **kwargs):
        created = bool(not self.id)

        super(Activity, self).save(*args, **kwargs)

        if not created:
            return

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F("num_comments") + 1)

    def delete(self, *args, **kwargs):
        super(Activity, self).delete(*args, **kwargs)

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F("num_comments") - 1)

    def send_notification(self):
        activity.send_activity_notifications.delay(self.id)
Пример #11
0
class Activity(Model):
    SET_RESOLVED = 1
    SET_UNRESOLVED = 2
    SET_MUTED = 3
    SET_PUBLIC = 4
    SET_PRIVATE = 5
    SET_REGRESSION = 6
    CREATE_ISSUE = 7
    NOTE = 8
    FIRST_SEEN = 9

    TYPE = (
        # (TYPE, verb-slug)
        (SET_RESOLVED, 'set_resolved'),
        (SET_UNRESOLVED, 'set_unresolved'),
        (SET_MUTED, 'set_muted'),
        (SET_PUBLIC, 'set_public'),
        (SET_PRIVATE, 'set_private'),
        (SET_REGRESSION, 'set_regression'),
        (CREATE_ISSUE, 'create_issue'),
        (NOTE, 'note'),
        (FIRST_SEEN, 'first_seen'),
    )

    project = models.ForeignKey('sentry.Project')
    group = models.ForeignKey('sentry.Group', null=True)
    event = models.ForeignKey('sentry.Event', null=True)
    # index on (type, ident)
    type = BoundedPositiveIntegerField(choices=TYPE)
    ident = models.CharField(max_length=64, null=True)
    # if the user is not set, it's assumed to be the system
    user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True)
    datetime = models.DateTimeField(default=timezone.now)
    data = GzippedDictField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_activity'

    __repr__ = sane_repr('project_id', 'group_id', 'event_id', 'user_id',
                         'type', 'ident')

    def save(self, *args, **kwargs):
        created = bool(not self.id)

        super(Activity, self).save(*args, **kwargs)

        if not created:
            return

        # HACK: support Group.num_comments
        if self.type == Activity.NOTE:
            self.group.update(num_comments=F('num_comments') + 1)

            if self.event:
                self.event.update(num_comments=F('num_comments') + 1)

    def send_notification(self):
        from sentry.models import User, UserOption
        from sentry.utils.email import MessageBuilder, group_id_to_email

        if self.type != Activity.NOTE or not self.group:
            return

        # TODO(dcramer): some of this logic is duplicated in NotificationPlugin
        # fetch access group members
        user_list = set(
            User.objects.filter(accessgroup__projects=self.project,
                                is_active=True).exclude(id=self.user_id, ))

        if self.project.team:
            # fetch team members
            user_list |= set(
                m.user for m in self.project.team.member_set.filter(
                    user__is_active=True, ).exclude(user__id=self.user_id, ))

        if not user_list:
            return

        disabled = set(
            UserOption.objects.filter(
                user__in=user_list,
                key='subscribe_comments',
                value='0',
            ).values_list('user', flat=True))

        send_to = [
            u.email for u in user_list if u.email and u.id not in disabled
        ]

        if not send_to:
            return

        author = self.user.first_name or self.user.username

        subject = '%s: %s' % (author, self.data['text'].splitlines()[0][:64])

        context = {
            'text': self.data['text'],
            'author': author,
            'group': self.group,
            'link': self.group.get_absolute_url(),
        }

        headers = {
            'X-Sentry-Reply-To': group_id_to_email(self.group.pk),
        }

        msg = MessageBuilder(
            subject=subject,
            context=context,
            template='sentry/emails/new_note.txt',
            html_template='sentry/emails/new_note.html',
            headers=headers,
        )

        try:
            msg.send(to=send_to)
        except Exception, e:
            logger = logging.getLogger('sentry.mail.errors')
            logger.exception(e)
Пример #12
0
class ContainerVersion(ExtensibleVersion):
    __core__ = True

    archetype = models.ForeignKey("clims.Container", related_name='versions')

    __repr__ = sane_repr('container_id', 'version', 'latest')
Пример #13
0
class AuditLogEntry(Model):
    organization = FlexibleForeignKey('sentry.Organization')
    actor = FlexibleForeignKey('sentry.User', related_name='audit_actors')
    target_object = BoundedPositiveIntegerField(null=True)
    target_user = FlexibleForeignKey('sentry.User', null=True, blank=True,
                                    related_name='audit_targets')
    event = BoundedPositiveIntegerField(choices=(
        # We emulate github a bit with event naming
        (AuditLogEntryEvent.MEMBER_INVITE, 'member.invite'),
        (AuditLogEntryEvent.MEMBER_ADD, 'member.add'),
        (AuditLogEntryEvent.MEMBER_ACCEPT, 'member.accept-invite'),
        (AuditLogEntryEvent.MEMBER_REMOVE, 'member.remove'),
        (AuditLogEntryEvent.MEMBER_EDIT, 'member.edit'),
        (AuditLogEntryEvent.MEMBER_JOIN_TEAM, 'member.join-team'),
        (AuditLogEntryEvent.MEMBER_LEAVE_TEAM, 'member.leave-team'),

        (AuditLogEntryEvent.TEAM_ADD, 'team.create'),
        (AuditLogEntryEvent.TEAM_EDIT, 'team.edit'),
        (AuditLogEntryEvent.TEAM_REMOVE, 'team.remove'),

        (AuditLogEntryEvent.PROJECT_ADD, 'project.create'),
        (AuditLogEntryEvent.PROJECT_EDIT, 'project.edit'),
        (AuditLogEntryEvent.PROJECT_REMOVE, 'project.remove'),
        (AuditLogEntryEvent.PROJECT_SET_PUBLIC, 'project.set-public'),
        (AuditLogEntryEvent.PROJECT_SET_PRIVATE, 'project.set-private'),

        (AuditLogEntryEvent.ORG_ADD, 'org.create'),
        (AuditLogEntryEvent.ORG_EDIT, 'org.edit'),

        (AuditLogEntryEvent.TAGKEY_REMOVE, 'tagkey.remove'),

        (AuditLogEntryEvent.PROJECTKEY_ADD, 'projectkey.create'),
        (AuditLogEntryEvent.PROJECTKEY_EDIT, 'projectkey.edit'),
        (AuditLogEntryEvent.PROJECTKEY_REMOVE, 'projectkey.remove'),
        (AuditLogEntryEvent.PROJECTKEY_ENABLE, 'projectkey.enable'),
        (AuditLogEntryEvent.PROJECTKEY_DISABLE, 'projectkey.disable'),

        (AuditLogEntryEvent.SSO_ENABLE, 'sso.enable'),
        (AuditLogEntryEvent.SSO_DISABLE, 'sso.disable'),
        (AuditLogEntryEvent.SSO_EDIT, 'sso.edit'),
        (AuditLogEntryEvent.SSO_IDENTITY_LINK, 'sso-identity.link'),

        (AuditLogEntryEvent.APIKEY_ADD, 'api-key.create'),
        (AuditLogEntryEvent.APIKEY_EDIT, 'api-key.edit'),
        (AuditLogEntryEvent.APIKEY_REMOVE, 'api-key.remove'),
    ))
    ip_address = models.GenericIPAddressField(null=True, unpack_ipv4=True)
    data = GzippedDictField()
    datetime = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_auditlogentry'

    __repr__ = sane_repr('organization_id', 'type')

    def get_note(self):
        if self.event == AuditLogEntryEvent.MEMBER_INVITE:
            return 'invited member %s' % (self.data['email'],)
        elif self.event == AuditLogEntryEvent.MEMBER_ADD:
            if self.target_user == self.actor:
                return 'joined the organization'
            return 'added member %s' % (self.target_user.get_display_name(),)
        elif self.event == AuditLogEntryEvent.MEMBER_ACCEPT:
            return 'accepted the membership invite'
        elif self.event == AuditLogEntryEvent.MEMBER_REMOVE:
            if self.target_user == self.actor:
                return 'left the organization'
            return 'removed member %s' % (self.data.get('email') or self.target_user.get_display_name(),)
        elif self.event == AuditLogEntryEvent.MEMBER_EDIT:
            return 'edited member %s' % (self.data.get('email') or self.target_user.get_display_name(),)
        elif self.event == AuditLogEntryEvent.MEMBER_JOIN_TEAM:
            if self.target_user == self.actor:
                return 'joined team %s' % (self.data['team_slug'],)
            return 'added %s to team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )
        elif self.event == AuditLogEntryEvent.MEMBER_LEAVE_TEAM:
            if self.target_user == self.actor:
                return 'left team %s' % (self.data['team_slug'],)
            return 'removed %s from team %s' % (
                self.data.get('email') or self.target_user.get_display_name(),
                self.data['team_slug'],
            )

        elif self.event == AuditLogEntryEvent.ORG_ADD:
            return 'created the organization'
        elif self.event == AuditLogEntryEvent.ORG_EDIT:
            return 'edited the organization'

        elif self.event == AuditLogEntryEvent.TEAM_ADD:
            return 'created team %s' % (self.data['slug'],)
        elif self.event == AuditLogEntryEvent.TEAM_EDIT:
            return 'edited team %s' % (self.data['slug'],)
        elif self.event == AuditLogEntryEvent.TEAM_REMOVE:
            return 'removed team %s' % (self.data['slug'],)

        elif self.event == AuditLogEntryEvent.PROJECT_ADD:
            return 'created project %s' % (self.data['slug'],)
        elif self.event == AuditLogEntryEvent.PROJECT_EDIT:
            return 'edited project %s' % (self.data['slug'],)
        elif self.event == AuditLogEntryEvent.PROJECT_REMOVE:
            return 'removed project %s' % (self.data['slug'],)

        elif self.event == AuditLogEntryEvent.TAGKEY_REMOVE:
            return 'removed tags matching %s = *' % (self.data['key'],)

        elif self.event == AuditLogEntryEvent.PROJECTKEY_ADD:
            return 'added project key %s' % (self.data['public_key'],)
        elif self.event == AuditLogEntryEvent.PROJECTKEY_EDIT:
            return 'edited project key %s' % (self.data['public_key'],)
        elif self.event == AuditLogEntryEvent.PROJECTKEY_REMOVE:
            return 'removed project key %s' % (self.data['public_key'],)
        elif self.event == AuditLogEntryEvent.PROJECTKEY_ENABLE:
            return 'enabled project key %s' % (self.data['public_key'],)
        elif self.event == AuditLogEntryEvent.PROJECTKEY_DISABLE:
            return 'disabled project key %s' % (self.data['public_key'],)

        elif self.event == AuditLogEntryEvent.SSO_ENABLE:
            return 'enabled sso (%s)' % (self.data['provider'],)
        elif self.event == AuditLogEntryEvent.SSO_DISABLE:
            return 'disabled sso (%s)' % (self.data['provider'],)
        elif self.event == AuditLogEntryEvent.SSO_EDIT:
            return 'edited sso settings'
        elif self.event == AuditLogEntryEvent.SSO_IDENTITY_LINK:
            return 'linked their account to a new identity'

        elif self.event == AuditLogEntryEvent.APIKEY_ADD:
            return 'added api key %s (%s)' % (self.data['label'], self.data['key'])
        elif self.event == AuditLogEntryEvent.APIKEY_EDIT:
            return 'edited api key %s (%s)' % (self.data['label'], self.data['key'])
        elif self.event == AuditLogEntryEvent.APIKEY_REMOVE:
            return 'removed api key %s (%s)' % (self.data['label'], self.data['key'])

        return ''
Пример #14
0
class Environment(Model):
    __core__ = False

    organization_id = BoundedPositiveIntegerField()
    projects = models.ManyToManyField('sentry.Project',
                                      through=EnvironmentProject)
    project_id = BoundedPositiveIntegerField(null=True)
    name = models.CharField(max_length=64)
    date_added = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_environment'
        unique_together = (
            ('project_id', 'name'),
            ('organization_id', 'name'),
        )

    __repr__ = sane_repr('organization_id', 'name')

    @classmethod
    def get_cache_key(cls, project_id, name):
        return 'env:1:%s:%s' % (project_id, md5_text(name).hexdigest())

    @classmethod
    def get_lock_key(cls, organization_id, name):
        return 'environment:%s:%s' % (organization_id,
                                      md5_text(name).hexdigest())

    @classmethod
    def get_or_create(cls, project, name):
        name = name or ''

        cache_key = cls.get_cache_key(project.id, name)

        env = cache.get(cache_key)
        if env is None:
            try:
                env = cls.objects.get(
                    projects=project,
                    organization_id=project.organization_id,
                    name=name,
                )
            except cls.DoesNotExist:
                env = cls.objects.filter(
                    organization_id=project.organization_id,
                    name=name,
                ).order_by('date_added').first()
                if not env:
                    lock_key = cls.get_lock_key(project.organization_id, name)
                    lock = locks.get(lock_key, duration=5)
                    with TimedRetryPolicy(10)(lock.acquire):
                        try:
                            env = cls.objects.get(
                                organization_id=project.organization_id,
                                name=name,
                            )
                        except cls.DoesNotExist:
                            env = cls.objects.create(
                                project_id=project.id,
                                name=name,
                                organization_id=project.organization_id)
                env.add_project(project)

            cache.set(cache_key, env, 3600)

        return env

    def add_project(self, project):
        try:
            with transaction.atomic():
                EnvironmentProject.objects.create(project=project,
                                                  environment=self)
        except IntegrityError:
            pass
Пример #15
0
class Environment(Model):
    __core__ = False

    organization_id = BoundedPositiveIntegerField()
    projects = models.ManyToManyField('sentry.Project',
                                      through=EnvironmentProject)
    # DEPRECATED, use projects
    project_id = BoundedPositiveIntegerField(null=True)
    name = models.CharField(max_length=64)
    date_added = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_environment'
        unique_together = (('organization_id', 'name'), )

    __repr__ = sane_repr('organization_id', 'name')

    @classmethod
    def is_valid_name(cls, value):
        """Limit length and reject problematic bytes

        If you change the rules here also update the event ingestion schema
        in sentry.interfaces.schemas
        """
        if len(value) > ENVIRONMENT_NAME_MAX_LENGTH:
            return False
        return OK_NAME_PATTERN.match(value) is not None

    @classmethod
    def get_cache_key(cls, organization_id, name):
        return 'env:2:%s:%s' % (organization_id, md5_text(name).hexdigest())

    @classmethod
    def get_name_or_default(cls, name):
        return name or ''

    @classmethod
    def get_for_organization_id(cls, organization_id, name):
        name = cls.get_name_or_default(name)

        cache_key = cls.get_cache_key(organization_id, name)

        env = cache.get(cache_key)
        if env is None:
            env = cls.objects.get(
                name=name,
                organization_id=organization_id,
            )
            cache.set(cache_key, env, 3600)

        return env

    @classmethod
    def get_or_create(cls, project, name):
        name = cls.get_name_or_default(name)

        cache_key = cls.get_cache_key(project.organization_id, name)

        env = cache.get(cache_key)
        if env is None:
            env = cls.objects.get_or_create(
                name=name,
                organization_id=project.organization_id,
            )[0]
            cache.set(cache_key, env, 3600)

        env.add_project(project)

        return env

    def add_project(self, project, is_hidden=None):
        cache_key = 'envproj:c:%s:%s' % (self.id, project.id)

        if cache.get(cache_key) is None:
            try:
                with transaction.atomic():
                    EnvironmentProject.objects.create(
                        project=project,
                        environment=self,
                        is_hidden=is_hidden,
                    )
                cache.set(cache_key, 1, 3600)
            except IntegrityError:
                # We've already created the object, should still cache the action.
                cache.set(cache_key, 1, 3600)

    @staticmethod
    def get_name_from_path_segment(segment):
        # In cases where the environment name is passed as a URL path segment,
        # the (case-sensitive) string "none" represents the empty string
        # environment name for historic reasons (see commit b09858f.) In all
        # other contexts (incl. request query string parameters), the empty
        # string should be used.
        return segment if segment != 'none' else ''
Пример #16
0
class ExtensibleProperty(Model):
    """
    A class for properties that can be hooked up to `Extensible` objects.

    Already implemented:
      * Bound to a `Substance`

    Will be implemented:
      * Bound to a `Container`
      * Bound to a `Project`

    Properties are versioned based on the version of the Extensible at the time they were
    bound to it.

    The type of a property is determined by a plugin when the system is upgraded. They are
    modelled with `ExtensiblePropertyType`.
    """
    __core__ = False

    extensible_property_type = models.ForeignKey("clims.ExtensiblePropertyType")

    # Supported values (TODO: add more)
    float_value = models.FloatField(null=True)
    int_value = models.IntegerField(null=True)
    string_value = models.TextField(null=True)
    bool_value = models.NullBooleanField(null=True)

    def __init__(self, *args, **kwargs):
        super(ExtensibleProperty, self).__init__(*args, **kwargs)

    def _get_field_from_type(self):
        return self.extensible_property_type.get_value_field()

    def _serialize(self, value):
        raw_type = self.extensible_property_type.raw_type
        if raw_type == ExtensiblePropertyType.JSON:
            return json.dumps(value)
        else:
            return value

    def _deserialize(self, value):
        raw_type = self.extensible_property_type.raw_type
        if raw_type == ExtensiblePropertyType.JSON:
            return json.loads(value)
        else:
            return value

    @property
    def name(self):
        if not self.extensible_property_type:
            return None
        return self.extensible_property_type.name

    @property
    def display_name(self):
        if not self.extensible_property_type:
            return None
        return self.extensible_property_type.display_name

    @property
    def value(self):
        field_name = self._get_field_from_type()
        return self._deserialize(getattr(self, field_name))

    @value.setter
    def value(self, val):
        field_name = self._get_field_from_type()
        setattr(self, field_name, self._serialize(val))

    class Meta:
        app_label = 'clims'
        db_table = 'clims_extensibleproperty'

    __repr__ = sane_repr('name', 'value', 'display_name')
Пример #17
0
class SnubaEvent(EventCommon):
    """
        An event backed by data stored in snuba.

        This is a readonly event and does not support event creation or save.
        The basic event data is fetched from snuba, and the event body is
        fetched from nodestore and bound to the data property in the same way
        as a regular Event.
    """

    # The minimal list of columns we need to get from snuba to bootstrap an
    # event. If the client is planning on loading the entire event body from
    # nodestore anyway, we may as well only fetch the minimum from snuba to
    # avoid duplicated work.
    minimal_columns = ["event_id", "group_id", "project_id", "timestamp"]

    __repr__ = sane_repr("project_id", "group_id")

    def __init__(self, snuba_values):
        """
            When initializing a SnubaEvent, think about the attributes you
            might need to access on it. If you only need a few properties, and
            they are all available in snuba, then you should use
            `SnubaEvent.selected_colums` (or a subset depending on your needs)
            But if you know you are going to need the entire event body anyway
            (which requires a nodestore lookup) you may as well just initialize
            the event with `SnubaEvent.minimal_colums` and let the rest of of
            the attributes come from nodestore.
        """
        assert all(k in snuba_values for k in SnubaEvent.minimal_columns)

        # self.snuba_data is a dict of all the stuff we got from snuba
        self.snuba_data = snuba_values

        # self.data is a (lazy) dict of everything we got from nodestore
        node_id = SnubaEvent.generate_node_id(self.snuba_data["project_id"],
                                              self.snuba_data["event_id"])
        self.data = NodeData(None, node_id, data=None, wrapper=EventDict)

    def __getattr__(self, name):
        """
        Depending on what snuba data this event was initialized with, we may
        have the data available to return, or we may have to look in the
        `data` dict (which would force a nodestore load). All unresolved
        self.foo type accesses will come through here.
        """
        if name in ("_project_cache", "_group_cache", "_environment_cache"):
            raise AttributeError()

        if name in self.snuba_data:
            return self.snuba_data[name]
        else:
            return self.data[name]

    # ============================================
    # Snuba-only implementations of properties that
    # would otherwise require nodestore data.
    # ============================================
    @property
    def tags(self):
        """
        Override of tags property that uses tags from snuba rather than
        the nodestore event body. This might be useful for implementing
        tag deletions without having to rewrite nodestore blobs.
        """
        if "tags.key" in self.snuba_data and "tags.value" in self.snuba_data:
            keys = getattr(self, "tags.key")
            values = getattr(self, "tags.value")
            if keys and values and len(keys) == len(values):
                return sorted(zip(keys, values))
            else:
                return []
        else:
            return super(SnubaEvent, self).tags

    def get_minimal_user(self):
        from sentry.interfaces.user import User

        return User.to_python({
            "id": self.user_id,
            "email": self.email,
            "username": self.username,
            "ip_address": self.ip_address,
        })

    # If the data for these is available from snuba, we assume
    # it was already normalized on the way in and we can just return
    # it, otherwise we defer to EventCommon implementation.
    def get_event_type(self):
        if "type" in self.snuba_data:
            return self.snuba_data["type"]
        return super(SnubaEvent, self).get_event_type()

    @property
    def ip_address(self):
        if "ip_address" in self.snuba_data:
            return self.snuba_data["ip_address"]
        return super(SnubaEvent, self).ip_address

    @property
    def title(self):
        if "title" in self.snuba_data:
            return self.snuba_data["title"]
        return super(SnubaEvent, self).title

    @property
    def culprit(self):
        if "culprit" in self.snuba_data:
            return self.snuba_data["culprit"]
        return super(SnubaEvent, self).culprit

    @property
    def location(self):
        if "location" in self.snuba_data:
            return self.snuba_data["location"]
        return super(SnubaEvent, self).location

    # ====================================================
    # Snuba implementations of the django fields on Event
    # ====================================================
    @property
    def datetime(self):
        """
        Reconstruct the datetime of this event from the snuba timestamp
        """
        # dateutil seems to use tzlocal() instead of UTC even though the string
        # ends with '+00:00', so just replace the TZ with UTC because we know
        # all timestamps from snuba are UTC.
        return parse_date(self.timestamp).replace(tzinfo=pytz.utc)

    @property
    def message(self):
        if "message" in self.snuba_data:
            return self.snuba_data["message"]
        return self.data.get("message")

    @property
    def platform(self):
        if "platform" in self.snuba_data:
            return self.snuba_data["platform"]
        return self.data.get("platform")

    @property
    def id(self):
        # Because a snuba event will never have a django row id, just return
        # the hex event_id here. We should be moving to a world where we never
        # have to reference the row id anyway.
        return self.event_id

    def save(self):
        raise NotImplementedError
Пример #18
0
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project', null=True)
    logger = models.CharField(
        max_length=64,
        blank=True,
        default=DEFAULT_LOGGER_NAME,
        db_index=True)
    level = BoundedPositiveIntegerField(
        choices=LOG_LEVELS.items(), default=logging.ERROR, blank=True, db_index=True
    )
    message = models.TextField()
    culprit = models.CharField(
        max_length=MAX_CULPRIT_LENGTH, blank=True, null=True, db_column='view'
    )
    num_comments = BoundedPositiveIntegerField(default=0, null=True)
    platform = models.CharField(max_length=64, null=True)
    status = BoundedPositiveIntegerField(
        default=0,
        choices=(
            (GroupStatus.UNRESOLVED, _('Unresolved')), (GroupStatus.RESOLVED, _('Resolved')),
            (GroupStatus.IGNORED, _('Ignored')),
        ),
        db_index=True
    )
    times_seen = BoundedPositiveIntegerField(default=1, db_index=True)
    last_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_seen = models.DateTimeField(default=timezone.now, db_index=True)
    first_release = FlexibleForeignKey('sentry.Release', null=True, on_delete=models.PROTECT)
    resolved_at = models.DateTimeField(null=True, db_index=True)
    # active_at should be the same as first_seen by default
    active_at = models.DateTimeField(null=True, db_index=True)
    time_spent_total = BoundedIntegerField(default=0)
    time_spent_count = BoundedIntegerField(default=0)
    score = BoundedIntegerField(default=0)
    # deprecated, do not use. GroupShare has superseded
    is_public = models.NullBooleanField(default=False, null=True)
    data = GzippedDictField(blank=True, null=True)
    short_id = BoundedBigIntegerField(null=True)

    objects = GroupManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_groupedmessage'
        verbose_name_plural = _('grouped messages')
        verbose_name = _('grouped message')
        permissions = (("can_view", "Can view"), )
        index_together = (('project', 'first_release'), )
        unique_together = (('project', 'short_id'), )

    __repr__ = sane_repr('project_id')

    def __unicode__(self):
        return "(%s) %s" % (self.times_seen, self.error())

    def save(self, *args, **kwargs):
        if not self.last_seen:
            self.last_seen = timezone.now()
        if not self.first_seen:
            self.first_seen = self.last_seen
        if not self.active_at:
            self.active_at = self.first_seen
        # We limit what we store for the message body
        self.message = strip(self.message)
        if self.message:
            self.message = truncatechars(self.message.splitlines()[0], 255)
        if self.times_seen is None:
            self.times_seen = 1
        self.score = type(self).calculate_score(
            times_seen=self.times_seen,
            last_seen=self.last_seen,
        )
        super(Group, self).save(*args, **kwargs)

    def get_absolute_url(self, params=None):
        from sentry import features
        if features.has('organizations:sentry10', self.organization):
            url = reverse('sentry-organization-issue', args=[self.organization.slug, self.id])
        else:
            url = reverse('sentry-group', args=[self.organization.slug, self.project.slug, self.id])
        if params:
            url = url + '?' + urlencode(params)
        return absolute_uri(url)

    @property
    def qualified_short_id(self):
        if self.short_id is not None:
            return '%s-%s' % (self.project.slug.upper(), base32_encode(self.short_id), )

    @property
    def event_set(self):
        from sentry.models import Event
        return Event.objects.filter(group_id=self.id)

    def is_over_resolve_age(self):
        resolve_age = self.project.get_option('sentry:resolve_age', None)
        if not resolve_age:
            return False
        return self.last_seen < timezone.now() - timedelta(hours=int(resolve_age))

    def is_ignored(self):
        return self.get_status() == GroupStatus.IGNORED

    # TODO(dcramer): remove in 9.0 / after plugins no long ref
    is_muted = is_ignored

    def is_resolved(self):
        return self.get_status() == GroupStatus.RESOLVED

    def get_status(self):
        # XXX(dcramer): GroupSerializer reimplements this logic
        from sentry.models import GroupSnooze

        status = self.status

        if status == GroupStatus.IGNORED:
            try:
                snooze = GroupSnooze.objects.get(group=self)
            except GroupSnooze.DoesNotExist:
                pass
            else:
                if not snooze.is_valid(group=self):
                    status = GroupStatus.UNRESOLVED

        if status == GroupStatus.UNRESOLVED and self.is_over_resolve_age():
            return GroupStatus.RESOLVED
        return status

    def get_share_id(self):
        from sentry.models import GroupShare
        try:
            return GroupShare.objects.filter(
                group_id=self.id,
            ).values_list('uuid', flat=True)[0]
        except IndexError:
            # Otherwise it has not been shared yet.
            return None

    @classmethod
    def from_share_id(cls, share_id):
        if not share_id or len(share_id) != 32:
            raise cls.DoesNotExist

        from sentry.models import GroupShare
        return cls.objects.get(
            id=GroupShare.objects.filter(
                uuid=share_id,
            ).values_list('group_id'),
        )

    def get_score(self):
        return type(self).calculate_score(self.times_seen, self.last_seen)

    def get_latest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_latest_event'):
            latest_events = sorted(
                Event.objects.filter(
                    group_id=self.id,
                ).order_by('-datetime')[0:5],
                key=EVENT_ORDERING_KEY,
                reverse=True,
            )
            try:
                self._latest_event = latest_events[0]
            except IndexError:
                self._latest_event = None
        return self._latest_event

    def get_latest_event_for_environments(self, environments=[]):
        use_snuba = options.get('snuba.events-queries.enabled')

        # Fetch without environment if Snuba is not enabled
        if not use_snuba:
            return self.get_latest_event()

        return get_oldest_or_latest_event_for_environments(
            EventOrdering.LATEST,
            environments=environments,
            issue_id=self.id,
            project_id=self.project_id)

    def get_oldest_event(self):
        from sentry.models import Event

        if not hasattr(self, '_oldest_event'):
            oldest_events = sorted(
                Event.objects.filter(
                    group_id=self.id,
                ).order_by('datetime')[0:5],
                key=EVENT_ORDERING_KEY,
            )
            try:
                self._oldest_event = oldest_events[0]
            except IndexError:
                self._oldest_event = None
        return self._oldest_event

    def get_oldest_event_for_environments(self, environments=[]):
        use_snuba = options.get('snuba.events-queries.enabled')

        # Fetch without environment if Snuba is not enabled
        if not use_snuba:
            return self.get_oldest_event()

        return get_oldest_or_latest_event_for_environments(
            EventOrdering.OLDEST,
            environments=environments,
            issue_id=self.id,
            project_id=self.project_id)

    def get_first_release(self):
        if self.first_release_id is None:
            return tagstore.get_first_release(self.project_id, self.id)

        return self.first_release.version

    def get_last_release(self):
        return tagstore.get_last_release(self.project_id, self.id)

    def get_event_type(self):
        """
        Return the type of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data.get('type', 'default')

    def get_event_metadata(self):
        """
        Return the metadata of this issue.

        See ``sentry.eventtypes``.
        """
        return self.data['metadata']

    @property
    def title(self):
        et = eventtypes.get(self.get_event_type())()
        return et.get_title(self.get_event_metadata())

    def location(self):
        et = eventtypes.get(self.get_event_type())()
        return et.get_location(self.get_event_metadata())

    def error(self):
        warnings.warn('Group.error is deprecated, use Group.title', DeprecationWarning)
        return self.title

    error.short_description = _('error')

    @property
    def message_short(self):
        warnings.warn('Group.message_short is deprecated, use Group.title', DeprecationWarning)
        return self.title

    @property
    def organization(self):
        return self.project.organization

    @property
    def checksum(self):
        warnings.warn('Group.checksum is no longer used', DeprecationWarning)
        return ''

    def get_email_subject(self):
        return '%s - %s' % (
            self.qualified_short_id.encode('utf-8'),
            self.title.encode('utf-8')
        )

    def count_users_seen(self):
        return tagstore.get_groups_user_counts(
            [self.project_id], [self.id], environment_ids=None)[self.id]

    @classmethod
    def calculate_score(cls, times_seen, last_seen):
        return math.log(float(times_seen or 1)) * 600 + float(last_seen.strftime('%s'))
Пример #19
0
class NotificationSetting(Model):
    """
    A setting of when to notify a user or team about activity within the app.
    Each row is a notification setting where a key is:
    ("scope_type", "scope_identifier", "target", "provider", "type"),
    and the value is ("value").
    """

    __include_in_export__ = False

    @property
    def scope_str(self) -> str:
        return get_notification_scope_name(self.scope_type)

    @property
    def type_str(self) -> str:
        return get_notification_setting_type_name(self.type)

    @property
    def value_str(self) -> str:
        return get_notification_setting_value_name(self.value)

    @property
    def provider_str(self) -> str:
        return get_provider_name(self.provider)

    scope_type = BoundedPositiveIntegerField(
        choices=(
            (NotificationScopeType.USER, "user"),
            (NotificationScopeType.ORGANIZATION, "organization"),
            (NotificationScopeType.PROJECT, "project"),
            (NotificationScopeType.TEAM, "team"),
        ),
        null=False,
    )
    # user_id, organization_id, project_id
    scope_identifier = BoundedBigIntegerField(null=False)
    target = FlexibleForeignKey("sentry.Actor",
                                db_index=True,
                                unique=False,
                                null=False,
                                on_delete=models.CASCADE)
    provider = BoundedPositiveIntegerField(
        choices=(
            (ExternalProviders.EMAIL, "email"),
            (ExternalProviders.SLACK, "slack"),
        ),
        null=False,
    )
    type = BoundedPositiveIntegerField(
        choices=(
            (NotificationSettingTypes.DEFAULT, "default"),
            (NotificationSettingTypes.DEPLOY, "deploy"),
            (NotificationSettingTypes.ISSUE_ALERTS, "issue"),
            (NotificationSettingTypes.WORKFLOW, "workflow"),
        ),
        null=False,
    )
    value = BoundedPositiveIntegerField(
        choices=(
            (NotificationSettingOptionValues.DEFAULT, "default"),
            (NotificationSettingOptionValues.NEVER, "off"),
            (NotificationSettingOptionValues.ALWAYS, "on"),
            (NotificationSettingOptionValues.SUBSCRIBE_ONLY, "subscribe_only"),
            (NotificationSettingOptionValues.COMMITTED_ONLY, "committed_only"),
        ),
        null=False,
    )

    objects = NotificationsManager()

    class Meta:
        app_label = "sentry"
        db_table = "sentry_notificationsetting"
        unique_together = ((
            "scope_type",
            "scope_identifier",
            "target",
            "provider",
            "type",
        ), )

    __repr__ = sane_repr(
        "scope_str",
        "scope_identifier",
        "target",
        "provider_str",
        "type_str",
        "value_str",
    )
Пример #20
0
class Project(Model, PendingDeletionMixin):
    """
    Projects are permission based namespaces which generally
    are the top level entry point for all data.
    """
    __core__ = True

    slug = models.SlugField(null=True)
    name = models.CharField(max_length=200)
    forced_color = models.CharField(max_length=6, null=True, blank=True)
    organization = FlexibleForeignKey('sentry.Organization')
    teams = models.ManyToManyField('sentry.Team',
                                   related_name='teams',
                                   through=ProjectTeam)
    public = models.BooleanField(default=False)
    date_added = models.DateTimeField(default=timezone.now)
    status = BoundedPositiveIntegerField(
        default=0,
        choices=(
            (ObjectStatus.VISIBLE, _('Active')),
            (ObjectStatus.PENDING_DELETION, _('Pending Deletion')),
            (ObjectStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
        ),
        db_index=True)
    # projects that were created before this field was present
    # will have their first_event field set to date_added
    first_event = models.DateTimeField(null=True)
    flags = BitField(flags=(('has_releases',
                             'This Project has sent release data'), ),
                     default=0,
                     null=True)

    objects = ProjectManager(cache_fields=[
        'pk',
        'slug',
    ])
    platform = models.CharField(max_length=64, null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_project'
        unique_together = (('organization', 'slug'), )

    __repr__ = sane_repr('team_id', 'name', 'slug')

    _rename_fields_on_pending_delete = frozenset(['slug'])

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.slug)

    def next_short_id(self):
        from sentry.models import Counter
        return Counter.increment(self)

    def save(self, *args, **kwargs):
        if not self.slug:
            lock = locks.get('slug:project', duration=5)
            with TimedRetryPolicy(10)(lock.acquire):
                slugify_instance(self,
                                 self.name,
                                 organization=self.organization,
                                 reserved=RESERVED_PROJECT_SLUGS)
            super(Project, self).save(*args, **kwargs)
        else:
            super(Project, self).save(*args, **kwargs)
        self.update_rev_for_option()

    def get_absolute_url(self, params=None):
        url = u'/organizations/{}/issues/'.format(self.organization.slug)
        params = {} if params is None else params
        params['project'] = self.id
        if params:
            url = url + '?' + urlencode(params)
        return absolute_uri(url)

    def is_internal_project(self):
        for value in (settings.SENTRY_FRONTEND_PROJECT,
                      settings.SENTRY_PROJECT):
            if six.text_type(self.id) == six.text_type(value) or six.text_type(
                    self.slug) == six.text_type(value):
                return True
        return False

    # TODO: Make these a mixin
    def update_option(self, *args, **kwargs):
        return projectoptions.set(self, *args, **kwargs)

    def get_option(self, *args, **kwargs):
        return projectoptions.get(self, *args, **kwargs)

    def delete_option(self, *args, **kwargs):
        return projectoptions.delete(self, *args, **kwargs)

    def update_rev_for_option(self):
        return projectoptions.update_rev_for_option(self)

    @property
    def callsign(self):
        warnings.warn(
            'Project.callsign is deprecated. Use Group.get_short_id() instead.',
            DeprecationWarning)
        return self.slug.upper()

    @property
    def color(self):
        if self.forced_color is not None:
            return '#%s' % self.forced_color
        return get_hashed_color(self.callsign or self.slug)

    @property
    def member_set(self):
        from sentry.models import OrganizationMember
        return self.organization.member_set.filter(
            id__in=OrganizationMember.objects.filter(
                organizationmemberteam__is_active=True,
                organizationmemberteam__team__in=self.teams.all(),
            ).values('id'),
            user__is_active=True,
        ).distinct()

    def has_access(self, user, access=None):
        from sentry.models import AuthIdentity, OrganizationMember

        warnings.warn('Project.has_access is deprecated.', DeprecationWarning)

        queryset = self.member_set.filter(user=user)

        if access is not None:
            queryset = queryset.filter(type__lte=access)

        try:
            member = queryset.get()
        except OrganizationMember.DoesNotExist:
            return False

        try:
            auth_identity = AuthIdentity.objects.get(
                auth_provider__organization=self.organization_id,
                user=member.user_id,
            )
        except AuthIdentity.DoesNotExist:
            return True

        return auth_identity.is_valid(member)

    def get_audit_log_data(self):
        return {
            'id': self.id,
            'slug': self.slug,
            'name': self.name,
            'status': self.status,
            'public': self.public,
        }

    def get_full_name(self):
        return self.slug

    def get_member_alert_settings(self, user_option):
        """
        Returns a list of users who have alert notifications explicitly
        enabled/disabled.
        :param user_option: alert option key, typically 'mail:alert'
        :return: A dictionary in format {<user_id>: <int_alert_value>}
        """
        from sentry.models import UserOption
        return {
            o.user_id: int(o.value)
            for o in UserOption.objects.filter(
                project=self,
                key=user_option,
            )
        }

    def get_notification_recipients(self, user_option):
        from sentry.models import UserOption
        alert_settings = self.get_member_alert_settings(user_option)
        disabled = set(u for u, v in six.iteritems(alert_settings) if v == 0)

        member_set = set(
            self.member_set.exclude(user__in=disabled, ).values_list(
                'user', flat=True))

        # determine members default settings
        members_to_check = set(u for u in member_set
                               if u not in alert_settings)
        if members_to_check:
            disabled = set((uo.user_id for uo in UserOption.objects.filter(
                key='subscribe_by_default',
                user__in=members_to_check,
            ) if uo.value == '0'))
            member_set = [x for x in member_set if x not in disabled]

        return member_set

    def get_mail_alert_subscribers(self):
        user_ids = self.get_notification_recipients('mail:alert')
        if not user_ids:
            return []
        from sentry.models import User
        return list(User.objects.filter(id__in=user_ids))

    def is_user_subscribed_to_mail_alerts(self, user):
        from sentry.models import UserOption
        is_enabled = UserOption.objects.get_value(user,
                                                  'mail:alert',
                                                  project=self)
        if is_enabled is None:
            is_enabled = UserOption.objects.get_value(user,
                                                      'subscribe_by_default',
                                                      '1') == '1'
        else:
            is_enabled = bool(is_enabled)
        return is_enabled

    def transfer_to(self, team=None, organization=None):
        # NOTE: this will only work properly if the new team is in a different
        # org than the existing one, which is currently the only use case in
        # production
        # TODO(jess): refactor this to make it an org transfer only
        from sentry.models import (
            Environment,
            EnvironmentProject,
            ProjectTeam,
            ReleaseProject,
            ReleaseProjectEnvironment,
            Rule,
        )

        if organization is None:
            organization = team.organization

        old_org_id = self.organization_id
        org_changed = old_org_id != organization.id

        self.organization = organization

        try:
            with transaction.atomic():
                self.update(organization=organization, )
        except IntegrityError:
            slugify_instance(self, self.name, organization=organization)
            self.update(
                slug=self.slug,
                organization=organization,
            )

        # Both environments and releases are bound at an organization level.
        # Due to this, when you transfer a project into another org, we have to
        # handle this behavior somehow. We really only have two options here:
        # * Copy over all releases/environments into the new org and handle de-duping
        # * Delete the bindings and let them reform with new data.
        # We're generally choosing to just delete the bindings since new data
        # flowing in will recreate links correctly. The tradeoff is that
        # historical data is lost, but this is a compromise we're willing to
        # take and a side effect of allowing this feature. There are exceptions
        # to this however, such as rules, which should maintain their
        # configuration when moved across organizations.
        if org_changed:
            for model in ReleaseProject, ReleaseProjectEnvironment, EnvironmentProject:
                model.objects.filter(project_id=self.id, ).delete()
            # this is getting really gross, but make sure there aren't lingering associations
            # with old orgs or teams
            ProjectTeam.objects.filter(
                project=self, team__organization_id=old_org_id).delete()

        rules_by_environment_id = defaultdict(set)
        for rule_id, environment_id in Rule.objects.filter(
                project_id=self.id, environment_id__isnull=False).values_list(
                    'id', 'environment_id'):
            rules_by_environment_id[environment_id].add(rule_id)

        environment_names = dict(
            Environment.objects.filter(
                id__in=rules_by_environment_id, ).values_list('id', 'name'))

        for environment_id, rule_ids in rules_by_environment_id.items():
            Rule.objects.filter(id__in=rule_ids).update(
                environment_id=Environment.get_or_create(
                    self,
                    environment_names[environment_id],
                ).id, )

        # ensure this actually exists in case from team was null
        if team is not None:
            self.add_team(team)

    def add_team(self, team):
        try:
            with transaction.atomic():
                ProjectTeam.objects.create(project=self, team=team)
        except IntegrityError:
            return False
        else:
            return True

    def remove_team(self, team):
        ProjectTeam.objects.filter(
            project=self,
            team=team,
        ).delete()

    def get_security_token(self):
        lock = locks.get(self.get_lock_key(), duration=5)
        with TimedRetryPolicy(10)(lock.acquire):
            security_token = self.get_option('sentry:token', None)
            if security_token is None:
                security_token = uuid1().hex
                self.update_option('sentry:token', security_token)
            return security_token

    def get_lock_key(self):
        return 'project_token:%s' % self.id

    def copy_settings_from(self, project_id):
        """
        Copies project level settings of the inputted project
        - General Settings
        - ProjectTeams
        - Alerts Settings and Rules
        - EnvironmentProjects
        - ProjectOwnership Rules and settings
        - Project Inbound Data Filters

        Returns True if the settings have successfully been copied over
        Returns False otherwise
        """
        from sentry.models import (EnvironmentProject, ProjectOption,
                                   ProjectOwnership, Rule)
        model_list = [EnvironmentProject, ProjectOwnership, ProjectTeam, Rule]

        project = Project.objects.get(id=project_id)
        try:
            with transaction.atomic():
                for model in model_list:
                    # remove all previous project settings
                    model.objects.filter(project_id=self.id, ).delete()

                    # add settings from other project to self
                    for setting in model.objects.filter(project_id=project_id):
                        setting.pk = None
                        setting.project_id = self.id
                        setting.save()

                options = ProjectOption.objects.get_all_values(project=project)
                for key, value in six.iteritems(options):
                    self.update_option(key, value)

        except IntegrityError as e:
            logging.exception('Error occurred during copy project settings.',
                              extra={
                                  'error': e.message,
                                  'project_to': self.id,
                                  'project_from': project_id,
                              })
            return False
        return True
Пример #21
0
class ProjectKey(Model):
    __core__ = True

    project = FlexibleForeignKey("sentry.Project", related_name="key_set")
    label = models.CharField(max_length=64, blank=True, null=True)
    public_key = models.CharField(max_length=32, unique=True, null=True)
    secret_key = models.CharField(max_length=32, unique=True, null=True)
    roles = BitField(
        flags=(
            # access to post events to the store endpoint
            ("store", "Event API access"),
            # read/write access to rest API
            ("api", "Web API access"),
        ),
        default=["store"],
    )
    status = BoundedPositiveIntegerField(
        default=0,
        choices=(
            (ProjectKeyStatus.ACTIVE, _("Active")),
            (ProjectKeyStatus.INACTIVE, _("Inactive")),
        ),
        db_index=True,
    )
    date_added = models.DateTimeField(default=timezone.now, null=True)

    rate_limit_count = BoundedPositiveIntegerField(null=True)
    rate_limit_window = BoundedPositiveIntegerField(null=True)

    objects = ProjectKeyManager(
        cache_fields=("public_key", "secret_key"),
        # store projectkeys in memcached for longer than other models,
        # specifically to make the relay_projectconfig endpoint faster.
        cache_ttl=60 * 30,
    )

    data = JSONField()

    # support legacy project keys in API
    scopes = (
        "project:read",
        "project:write",
        "project:admin",
        "project:releases",
        "event:read",
        "event:write",
        "event:admin",
    )

    class Meta:
        app_label = "sentry"
        db_table = "sentry_projectkey"

    __repr__ = sane_repr("project_id", "public_key")

    def __str__(self):
        return str(self.public_key)

    @classmethod
    def generate_api_key(cls):
        return uuid4().hex

    @classmethod
    def looks_like_api_key(cls, key):
        return bool(_uuid4_re.match(key))

    @classmethod
    def from_dsn(cls, dsn):
        urlparts = urlparse(dsn)

        public_key = urlparts.username
        project_id = urlparts.path.rsplit("/", 1)[-1]

        try:
            return ProjectKey.objects.get(public_key=public_key,
                                          project=project_id)
        except ValueError:
            # ValueError would come from a non-integer project_id,
            # which is obviously a DoesNotExist. We catch and rethrow this
            # so anything downstream expecting DoesNotExist works fine
            raise ProjectKey.DoesNotExist(
                "ProjectKey matching query does not exist.")

    @classmethod
    def get_default(cls, project):
        return cls.objects.filter(
            project=project,
            roles=models.F("roles").bitor(cls.roles.store),
            status=ProjectKeyStatus.ACTIVE,
        ).first()

    @property
    def is_active(self):
        return self.status == ProjectKeyStatus.ACTIVE

    @property
    def rate_limit(self):
        if self.rate_limit_count and self.rate_limit_window:
            return (self.rate_limit_count, self.rate_limit_window)
        return (0, 0)

    def save(self, *args, **kwargs):
        if not self.public_key:
            self.public_key = ProjectKey.generate_api_key()
        if not self.secret_key:
            self.secret_key = ProjectKey.generate_api_key()
        if not self.label:
            self.label = petname.Generate(2, " ", letters=10).title()
        super().save(*args, **kwargs)

    def get_dsn(self, domain=None, secure=True, public=False):
        urlparts = urlparse(self.get_endpoint(public=public))

        if not public:
            key = f"{self.public_key}:{self.secret_key}"
        else:
            key = self.public_key

        # If we do not have a scheme or domain/hostname, dsn is never valid
        if not urlparts.netloc or not urlparts.scheme:
            return ""

        return "{}://{}@{}/{}".format(
            urlparts.scheme,
            key,
            urlparts.netloc + urlparts.path,
            self.project_id,
        )

    @property
    def organization_id(self):
        return self.project.organization_id

    @property
    def organization(self):
        return self.project.organization

    @property
    def dsn_private(self):
        return self.get_dsn(public=False)

    @property
    def dsn_public(self):
        return self.get_dsn(public=True)

    @property
    def csp_endpoint(self):
        endpoint = self.get_endpoint()

        return f"{endpoint}/api/{self.project_id}/csp-report/?sentry_key={self.public_key}"

    @property
    def security_endpoint(self):
        endpoint = self.get_endpoint()

        return f"{endpoint}/api/{self.project_id}/security/?sentry_key={self.public_key}"

    @property
    def minidump_endpoint(self):
        endpoint = self.get_endpoint()

        return f"{endpoint}/api/{self.project_id}/minidump/?sentry_key={self.public_key}"

    @property
    def unreal_endpoint(self):
        return f"{self.get_endpoint()}/api/{self.project_id}/unreal/{self.public_key}/"

    @property
    def js_sdk_loader_cdn_url(self):
        if settings.JS_SDK_LOADER_CDN_URL:
            return f"{settings.JS_SDK_LOADER_CDN_URL}{self.public_key}.min.js"
        else:
            endpoint = self.get_endpoint()
            return "{}{}".format(
                endpoint,
                reverse("sentry-js-sdk-loader", args=[self.public_key,
                                                      ".min"]),
            )

    def get_endpoint(self, public=True):
        if public:
            endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
        else:
            endpoint = settings.SENTRY_ENDPOINT

        if not endpoint:
            endpoint = options.get("system.url-prefix")

        if features.has("organizations:org-subdomains",
                        self.project.organization):
            urlparts = urlparse(endpoint)
            if urlparts.scheme and urlparts.netloc:
                endpoint = "{}://{}.{}{}".format(
                    urlparts.scheme,
                    settings.SENTRY_ORG_SUBDOMAIN_TEMPLATE.format(
                        organization_id=self.project.organization_id),
                    urlparts.netloc,
                    urlparts.path,
                )

        return endpoint

    def get_allowed_origins(self):
        from sentry.utils.http import get_origins

        return get_origins(self.project)

    def get_audit_log_data(self):
        return {
            "label": self.label,
            "public_key": self.public_key,
            "secret_key": self.secret_key,
            "roles": int(self.roles),
            "status": self.status,
            "rate_limit_count": self.rate_limit_count,
            "rate_limit_window": self.rate_limit_window,
        }

    def get_scopes(self):
        return self.scopes
Пример #22
0
class SnubaEvent(EventCommon):
    """
        An event backed by data stored in snuba.

        This is a readonly event and does not support event creation or save.
        The basic event data is fetched from snuba, and the event body is
        fetched from nodestore and bound to the data property in the same way
        as a regular Event.
    """

    # The list of columns that we should request from snuba to be able to fill
    # out the object.
    selected_columns = [
        'event_id',
        'project_id',
        'message',
        'title',
        'type',
        'location',
        'culprit',
        'timestamp',
        'group_id',
        'platform',

        # Required to provide snuba-only tags
        'tags.key',
        'tags.value',

        # Required to provide snuba-only 'user' interface
        'user_id',
        'username',
        'ip_address',
        'email',
    ]

    objects = SnubaEventManager()

    __repr__ = sane_repr('project_id', 'group_id')

    @classmethod
    def get_event(cls, project_id, event_id):
        from sentry.utils import snuba
        result = snuba.raw_query(
            start=datetime.utcfromtimestamp(
                0),  # will be clamped to project retention
            end=datetime.utcnow(),  # will be clamped to project retention
            selected_columns=cls.selected_columns,
            filter_keys={
                'event_id': [event_id],
                'project_id': [project_id],
            },
        )
        if 'error' not in result and len(result['data']) == 1:
            return SnubaEvent(result['data'][0])
        return None

    def __init__(self, snuba_values):
        assert set(snuba_values.keys()) == set(self.selected_columns)

        self.__dict__ = snuba_values

        # This should be lazy loaded and will only be accessed if we access any
        # properties on self.data
        node_id = SnubaEvent.generate_node_id(self.project_id, self.event_id)
        self.data = NodeData(None, node_id, data=None)

    # ============================================
    # Snuba-only implementations of properties that
    # would otherwise require nodestore data.
    # ============================================
    @property
    def tags(self):
        """
        Override of tags property that uses tags from snuba rather than
        the nodestore event body. This might be useful for implementing
        tag deletions without having to rewrite nodestore blobs.
        """
        keys = getattr(self, 'tags.key', None)
        values = getattr(self, 'tags.value', None)
        if keys and values and len(keys) == len(values):
            return sorted(zip(keys, values))
        return []

    def get_event_type(self):
        return self.__dict__.get('type', 'default')

    def get_minimal_user(self):
        from sentry.interfaces.user import User
        return User.to_python({
            'id': self.user_id,
            'email': self.email,
            'username': self.username,
            'ip_address': self.ip_address,
        })

    # These should all have been normalized to the correct values on
    # the way in to snuba, so we should be able to just use them as is.
    @property
    def ip_address(self):
        return self.__dict__['ip_address']

    @property
    def title(self):
        return self.__dict__['title']

    @property
    def culprit(self):
        return self.__dict__['culprit']

    @property
    def location(self):
        return self.__dict__['location']

    # ============================================
    # Snuba implementations of django Fields
    # ============================================
    @property
    def datetime(self):
        """
        Reconstruct the datetime of this event from the snuba timestamp
        """
        # dateutil seems to use tzlocal() instead of UTC even though the string
        # ends with '+00:00', so just replace the TZ with UTC because we know
        # all timestamps from snuba are UTC.
        return parse_date(self.timestamp).replace(tzinfo=pytz.utc)

    @property
    def time_spent(self):
        return None

    @property
    def id(self):
        # Because a snuba event will never have a django row id, just return
        # the hex event_id here. We should be moving to a world where we never
        # have to reference the row id anyway.
        return self.event_id

    def next_event_id(self, environments=None):
        from sentry.utils import snuba

        conditions = [['timestamp', '>=', self.timestamp],
                      [['timestamp', '>', self.timestamp],
                       ['event_id', '>', self.event_id]]]

        if environments:
            conditions.append(['environment', 'IN', environments])

        result = snuba.raw_query(
            start=self.datetime,  # gte current event
            end=datetime.utcnow(),  # will be clamped to project retention
            selected_columns=['event_id'],
            conditions=conditions,
            filter_keys={
                'project_id': [self.project_id],
                'issue': [self.group_id],
            },
            orderby=['timestamp', 'event_id'],
            limit=1)

        if 'error' in result or len(result['data']) == 0:
            return None

        return six.text_type(result['data'][0]['event_id'])

    def prev_event_id(self, environments=None):
        from sentry.utils import snuba

        conditions = [['timestamp', '<=', self.timestamp],
                      [['timestamp', '<', self.timestamp],
                       ['event_id', '<', self.event_id]]]

        if environments:
            conditions.append(['environment', 'IN', environments])

        result = snuba.raw_query(
            start=datetime.utcfromtimestamp(
                0),  # will be clamped to project retention
            end=self.datetime,  # lte current event
            selected_columns=['event_id'],
            conditions=conditions,
            filter_keys={
                'project_id': [self.project_id],
                'issue': [self.group_id],
            },
            orderby=['-timestamp', '-event_id'],
            limit=1)

        if 'error' in result or len(result['data']) == 0:
            return None

        return six.text_type(result['data'][0]['event_id'])

    def save(self):
        raise NotImplementedError
Пример #23
0
class ReleaseEnvironment(Model):
    __core__ = False

    organization = FlexibleForeignKey("sentry.Organization",
                                      db_index=True,
                                      db_constraint=False)
    # DEPRECATED
    project_id = BoundedPositiveIntegerField(null=True)
    release = FlexibleForeignKey("sentry.Release",
                                 db_index=True,
                                 db_constraint=False)
    environment = FlexibleForeignKey("sentry.Environment",
                                     db_index=True,
                                     db_constraint=False)
    first_seen = models.DateTimeField(default=timezone.now)
    last_seen = models.DateTimeField(default=timezone.now, db_index=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_environmentrelease"
        unique_together = (("organization", "release", "environment"), )

    __repr__ = sane_repr("organization_id", "release_id", "environment_id")

    @classmethod
    def get_cache_key(cls, organization_id, release_id, environment_id):
        return "releaseenv:2:{}:{}:{}".format(organization_id, release_id,
                                              environment_id)

    @classmethod
    def get_or_create(cls, project, release, environment, datetime, **kwargs):
        with metrics.timer(
                "models.releaseenvironment.get_or_create") as metric_tags:
            return cls._get_or_create_impl(project, release, environment,
                                           datetime, metric_tags)

    @classmethod
    def _get_or_create_impl(cls, project, release, environment, datetime,
                            metric_tags):
        cache_key = cls.get_cache_key(project.id, release.id, environment.id)

        instance = cache.get(cache_key)
        if instance is None:
            metric_tags["cache_hit"] = "false"
            instance, created = cls.objects.get_or_create(
                release_id=release.id,
                organization_id=project.organization_id,
                environment_id=environment.id,
                defaults={
                    "first_seen": datetime,
                    "last_seen": datetime
                },
            )
            cache.set(cache_key, instance, 3600)
        else:
            metric_tags["cache_hit"] = "true"
            created = False

        metric_tags["created"] = "true" if created else "false"

        # TODO(dcramer): this would be good to buffer, but until then we minimize
        # updates to once a minute, and allow Postgres to optimistically skip
        # it even if we can't
        if not created and instance.last_seen < datetime - timedelta(
                seconds=60):
            metric_tags["bumped"] = "true"
            cls.objects.filter(
                id=instance.id, last_seen__lt=datetime -
                timedelta(seconds=60)).update(last_seen=datetime)
            instance.last_seen = datetime
            cache.set(cache_key, instance, 3600)
        else:
            metric_tags["bumped"] = "false"

        return instance
Пример #24
0
class Event(EventCommon, Model):
    """
    An event backed by data stored in postgres.

    """
    __core__ = False

    group_id = BoundedBigIntegerField(blank=True, null=True)
    event_id = models.CharField(max_length=32,
                                null=True,
                                db_column="message_id")
    project_id = BoundedBigIntegerField(blank=True, null=True)
    message = models.TextField()
    platform = models.CharField(max_length=64, null=True)
    datetime = models.DateTimeField(default=timezone.now, db_index=True)
    time_spent = BoundedIntegerField(null=True)
    data = NodeField(
        blank=True,
        null=True,
        ref_func=lambda x: x.project_id or x.project.id,
        ref_version=2,
        wrapper=EventDict,
    )

    objects = EventManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_message'
        verbose_name = _('message')
        verbose_name_plural = _('messages')
        unique_together = (('project_id', 'event_id'), )
        index_together = (('group_id', 'datetime'), )

    __repr__ = sane_repr('project_id', 'group_id')

    def __getstate__(self):
        state = Model.__getstate__(self)

        # do not pickle cached info.  We want to fetch this on demand
        # again.  In particular if we were to pickle interfaces we would
        # pickle a CanonicalKeyView which old sentry workers do not know
        # about
        state.pop('_project_cache', None)
        state.pop('_group_cache', None)
        state.pop('interfaces', None)

        return state

    # Find next and previous events based on datetime and id. We cannot
    # simply `ORDER BY (datetime, id)` as this is too slow (no index), so
    # we grab the next 5 / prev 5 events by datetime, and sort locally to
    # get the next/prev events. Given that timestamps only have 1-second
    # granularity, this will be inaccurate if there are more than 5 events
    # in a given second.
    def next_event_id(self, environments=None):
        events = self.__class__.objects.filter(
            datetime__gte=self.datetime,
            group_id=self.group_id,
        ).exclude(id=self.id).order_by('datetime')[0:5]

        events = [
            e for e in events if e.datetime == self.datetime and e.id > self.id
            or e.datetime > self.datetime
        ]
        events.sort(key=EVENT_ORDERING_KEY)
        return six.text_type(events[0].event_id) if events else None

    def prev_event_id(self, environments=None):
        events = self.__class__.objects.filter(
            datetime__lte=self.datetime,
            group_id=self.group_id,
        ).exclude(id=self.id).order_by('-datetime')[0:5]

        events = [
            e for e in events if e.datetime == self.datetime and e.id < self.id
            or e.datetime < self.datetime
        ]
        events.sort(key=EVENT_ORDERING_KEY, reverse=True)
        return six.text_type(events[0].event_id) if events else None
Пример #25
0
class Repository(Model, PendingDeletionMixin):
    __core__ = True

    organization_id = BoundedPositiveIntegerField(db_index=True)
    name = models.CharField(max_length=200)
    url = models.URLField(null=True)
    provider = models.CharField(max_length=64, null=True)
    external_id = models.CharField(max_length=64, null=True)
    config = JSONField(default=dict)
    status = BoundedPositiveIntegerField(default=ObjectStatus.VISIBLE,
                                         choices=ObjectStatus.as_choices(),
                                         db_index=True)
    date_added = models.DateTimeField(default=timezone.now)
    integration_id = BoundedPositiveIntegerField(db_index=True, null=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_repository"
        unique_together = (
            ("organization_id", "name"),
            ("organization_id", "provider", "external_id"),
        )

    __repr__ = sane_repr("organization_id", "name", "provider")

    _rename_fields_on_pending_delete = frozenset(["name", "external_id"])

    def has_integration_provider(self):
        return self.provider and self.provider.startswith("integrations:")

    def get_provider(self):
        from sentry.plugins.base import bindings

        if self.has_integration_provider():
            provider_cls = bindings.get("integration-repository.provider").get(
                self.provider)
            return provider_cls(self.provider)

        provider_cls = bindings.get("repository.provider").get(self.provider)
        return provider_cls(self.provider)

    def generate_delete_fail_email(self, error_message):
        from sentry.utils.email import MessageBuilder

        new_context = {
            "repo": self,
            "error_message": error_message,
            "provider_name": self.get_provider().name,
        }

        return MessageBuilder(
            subject="Unable to Delete Repository Webhooks",
            context=new_context,
            template="sentry/emails/unable-to-delete-repo.txt",
            html_template="sentry/emails/unable-to-delete-repo.html",
        )

    def rename_on_pending_deletion(self, fields=None):
        # Due to the fact that Repository is shown to the user
        # as it is pending deletion, this is added to display the fields
        # correctly to the user.
        self.config["pending_deletion_name"] = self.name
        super(Repository, self).rename_on_pending_deletion(fields, ["config"])

    def reset_pending_deletion_field_names(self):
        del self.config["pending_deletion_name"]
        super(Repository, self).reset_pending_deletion_field_names(["config"])
Пример #26
0
class OrganizationMember(Model):
    """
    Identifies relationships between organizations and users.

    Users listed as team members are considered to have access to all projects
    and could be thought of as team owners (though their access level may not)
    be set to ownership.
    """

    __core__ = True

    organization = FlexibleForeignKey("sentry.Organization",
                                      related_name="member_set")

    user = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                              null=True,
                              blank=True,
                              related_name="sentry_orgmember_set")
    email = models.EmailField(null=True, blank=True, max_length=75)
    role = models.CharField(choices=roles.get_choices(),
                            max_length=32,
                            default=roles.get_default().id)
    flags = BitField(flags=(("sso:linked", "sso:linked"), ("sso:invalid",
                                                           "sso:invalid")),
                     default=0)
    token = models.CharField(max_length=64, null=True, blank=True, unique=True)
    date_added = models.DateTimeField(default=timezone.now)
    token_expires_at = models.DateTimeField(default=None, null=True)
    has_global_access = models.BooleanField(default=True)
    teams = models.ManyToManyField("sentry.Team",
                                   blank=True,
                                   through="sentry.OrganizationMemberTeam")
    inviter = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                                 null=True,
                                 blank=True,
                                 related_name="sentry_inviter_set")
    invite_status = models.PositiveSmallIntegerField(
        choices=(
            (InviteStatus.APPROVED.value, _("Approved")),
            (
                InviteStatus.REQUESTED_TO_BE_INVITED.value,
                _("Organization member requested to invite user"),
            ),
            (InviteStatus.REQUESTED_TO_JOIN.value,
             _("User requested to join organization")),
        ),
        default=InviteStatus.APPROVED.value,
        null=True,
    )

    # Deprecated -- no longer used
    type = BoundedPositiveIntegerField(default=50, blank=True)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_organizationmember"
        unique_together = (("organization", "user"), ("organization", "email"))

    __repr__ = sane_repr("organization_id", "user_id", "role")

    @transaction.atomic
    def save(self, *args, **kwargs):
        assert self.user_id or self.email, "Must set user or email"
        if self.token and not self.token_expires_at:
            self.refresh_expires_at()
        super(OrganizationMember, self).save(*args, **kwargs)

    def set_user(self, user):
        self.user = user
        self.email = None
        self.token = None
        self.token_expires_at = None

    def remove_user(self):
        self.email = self.get_email()
        self.user = None
        self.token = self.generate_token()

    def regenerate_token(self):
        self.token = self.generate_token()
        self.refresh_expires_at()

    def refresh_expires_at(self):
        now = timezone.now()
        self.token_expires_at = now + timedelta(days=INVITE_DAYS_VALID)

    def approve_invite(self):
        self.invite_status = InviteStatus.APPROVED.value
        self.regenerate_token()

    def get_invite_status_name(self):
        if self.invite_status is None:
            return
        return invite_status_names[self.invite_status]

    @property
    def invite_approved(self):
        return self.invite_status == InviteStatus.APPROVED.value

    @property
    def requested_to_join(self):
        return self.invite_status == InviteStatus.REQUESTED_TO_JOIN.value

    @property
    def requested_to_be_invited(self):
        return self.invite_status == InviteStatus.REQUESTED_TO_BE_INVITED.value

    @property
    def is_pending(self):
        return self.user_id is None

    @property
    def token_expired(self):
        # Old tokens don't expire to preserve compatibility and not require
        # a backfill migration.
        if self.token_expires_at is None:
            return False
        if self.token_expires_at > timezone.now():
            return False
        return True

    @property
    def legacy_token(self):
        checksum = md5()
        checksum.update(six.text_type(self.organization_id).encode("utf-8"))
        checksum.update(self.get_email().encode("utf-8"))
        checksum.update(force_bytes(settings.SECRET_KEY))
        return checksum.hexdigest()

    def generate_token(self):
        return uuid4().hex + uuid4().hex

    def get_invite_link(self):
        if not self.is_pending or not self.invite_approved:
            return None
        return absolute_uri(
            reverse(
                "sentry-accept-invite",
                kwargs={
                    "member_id": self.id,
                    "token": self.token or self.legacy_token
                },
            ))

    def send_invite_email(self):
        from sentry.utils.email import MessageBuilder

        context = {
            "email": self.email,
            "organization": self.organization,
            "url": self.get_invite_link(),
        }

        msg = MessageBuilder(
            subject="Join %s in using Sentry" % self.organization.name,
            template="sentry/emails/member-invite.txt",
            html_template="sentry/emails/member-invite.html",
            type="organization.invite",
            context=context,
        )

        try:
            msg.send_async([self.get_email()])
        except Exception as e:
            logger = get_logger(name="sentry.mail")
            logger.exception(e)

    def send_request_notification_email(self):
        from sentry.utils.email import MessageBuilder

        link_args = {"organization_slug": self.organization.slug}

        context = {
            "email":
            self.email,
            "inviter":
            self.inviter,
            "organization":
            self.organization,
            "organization_link":
            absolute_uri(
                reverse("sentry-organization-index",
                        args=[self.organization.slug])),
            "pending_requests_link":
            absolute_uri(
                reverse("sentry-organization-members-requests",
                        kwargs=link_args)),
        }

        if self.requested_to_join:
            email_args = {
                "template": "sentry/emails/organization-join-request.txt",
                "html_template":
                "sentry/emails/organization-join-request.html",
            }
        elif self.requested_to_be_invited:
            email_args = {
                "template": "sentry/emails/organization-invite-request.txt",
                "html_template":
                "sentry/emails/organization-invite-request.html",
            }
        else:
            raise RuntimeError("This member is not pending invitation")

        recipients = OrganizationMember.objects.select_related("user").filter(
            organization_id=self.organization_id,
            user__isnull=False,
            invite_status=InviteStatus.APPROVED.value,
            role__in=(r.id for r in roles.get_all()
                      if r.has_scope("member:write")),
        )

        msg = MessageBuilder(subject="Access request to %s" %
                             (self.organization.name, ),
                             type="organization.invite-request",
                             context=context,
                             **email_args)

        for recipient in recipients:
            try:
                msg.send_async([recipient.get_email()])
            except Exception as e:
                logger = get_logger(name="sentry.mail")
                logger.exception(e)

    def send_sso_link_email(self, actor, provider):
        from sentry.utils.email import MessageBuilder

        link_args = {"organization_slug": self.organization.slug}

        context = {
            "organization":
            self.organization,
            "actor":
            actor,
            "provider":
            provider,
            "url":
            absolute_uri(reverse("sentry-auth-organization",
                                 kwargs=link_args)),
        }

        msg = MessageBuilder(
            subject="Action Required for %s" % (self.organization.name, ),
            template="sentry/emails/auth-link-identity.txt",
            html_template="sentry/emails/auth-link-identity.html",
            type="organization.auth_link",
            context=context,
        )
        msg.send_async([self.get_email()])

    def send_sso_unlink_email(self, actor, provider):
        from sentry.utils.email import MessageBuilder
        from sentry.models import LostPasswordHash

        email = self.get_email()

        recover_uri = u"{path}?{query}".format(
            path=reverse("sentry-account-recover"),
            query=urlencode({"email": email}))

        # Nothing to send if this member isn't associated to a user
        if not self.user_id:
            return

        context = {
            "email": email,
            "recover_url": absolute_uri(recover_uri),
            "has_password": self.user.password,
            "organization": self.organization,
            "actor": actor,
            "provider": provider,
        }

        if not self.user.password:
            password_hash = LostPasswordHash.for_user(self.user)
            context["set_password_url"] = password_hash.get_absolute_url(
                mode="set_password")

        msg = MessageBuilder(
            subject="Action Required for %s" % (self.organization.name, ),
            template="sentry/emails/auth-sso-disabled.txt",
            html_template="sentry/emails/auth-sso-disabled.html",
            type="organization.auth_sso_disabled",
            context=context,
        )
        msg.send_async([email])

    def get_display_name(self):
        if self.user_id:
            return self.user.get_display_name()
        return self.email

    def get_label(self):
        if self.user_id:
            return self.user.get_label()
        return self.email or self.id

    def get_email(self):
        if self.user_id and self.user.email:
            return self.user.email
        return self.email

    def get_avatar_type(self):
        if self.user_id:
            return self.user.get_avatar_type()
        return "letter_avatar"

    def get_audit_log_data(self):
        from sentry.models import Team

        teams = list(
            Team.objects.filter(id__in=OrganizationMemberTeam.objects.filter(
                organizationmember=self, is_active=True).values_list(
                    "team", flat=True)).values("id", "slug"))

        return {
            "email": self.get_email(),
            "user": self.user_id,
            "teams": [t["id"] for t in teams],
            "teams_slugs": [t["slug"] for t in teams],
            "has_global_access": self.has_global_access,
            "role": self.role,
            "invite_status": invite_status_names[self.invite_status],
        }

    def get_teams(self):
        from sentry.models import Team

        return Team.objects.filter(
            status=TeamStatus.VISIBLE,
            id__in=OrganizationMemberTeam.objects.filter(
                organizationmember=self, is_active=True).values("team"),
        )

    def get_scopes(self):
        return roles.get(self.role).scopes

    @classmethod
    def delete_expired(cls, threshold):
        """
        Delete un-accepted member invitations that expired
        ``threshold`` days ago.
        """
        cls.objects.filter(
            token_expires_at__lt=threshold,
            user_id__exact=None).exclude(email__exact=None).delete()
Пример #27
0
class Team(Model):
    """
    A team represents a group of individuals which maintain ownership of projects.
    """

    __include_in_export__ = True

    organization = FlexibleForeignKey("sentry.Organization")
    slug = models.SlugField()
    name = models.CharField(max_length=64)
    status = BoundedPositiveIntegerField(
        choices=(
            (TeamStatus.VISIBLE, _("Active")),
            (TeamStatus.PENDING_DELETION, _("Pending Deletion")),
            (TeamStatus.DELETION_IN_PROGRESS, _("Deletion in Progress")),
        ),
        default=TeamStatus.VISIBLE,
    )
    actor = FlexibleForeignKey("sentry.Actor",
                               db_index=True,
                               unique=True,
                               null=True,
                               on_delete=models.PROTECT)
    date_added = models.DateTimeField(default=timezone.now, null=True)

    objects = TeamManager(cache_fields=("pk", "slug"))

    class Meta:
        app_label = "sentry"
        db_table = "sentry_team"
        unique_together = (("organization", "slug"), )

    __repr__ = sane_repr("name", "slug")

    def __str__(self):
        return f"{self.name} ({self.slug})"

    def save(self, *args, **kwargs):
        if not self.slug:
            lock = locks.get("slug:team", duration=5)
            with TimedRetryPolicy(10)(lock.acquire):
                slugify_instance(self,
                                 self.name,
                                 organization=self.organization)
        super().save(*args, **kwargs)

    @property
    def member_set(self):
        """:returns a QuerySet of all Users that belong to this Team"""
        return self.organization.member_set.filter(
            organizationmemberteam__team=self,
            organizationmemberteam__is_active=True,
            user__is_active=True,
        ).distinct()

    def has_access(self, user, access=None):
        from sentry.models import AuthIdentity, OrganizationMember

        warnings.warn("Team.has_access is deprecated.", DeprecationWarning)

        queryset = self.member_set.filter(user=user)
        if access is not None:
            queryset = queryset.filter(type__lte=access)

        try:
            member = queryset.get()
        except OrganizationMember.DoesNotExist:
            return False

        try:
            auth_identity = AuthIdentity.objects.get(
                auth_provider__organization=self.organization_id,
                user=member.user_id)
        except AuthIdentity.DoesNotExist:
            return True

        return auth_identity.is_valid(member)

    def transfer_to(self, organization):
        """
        Transfers a team and all projects under it to the given organization.
        """
        from sentry.models import (
            OrganizationAccessRequest,
            OrganizationMember,
            OrganizationMemberTeam,
            Project,
            ProjectTeam,
            ReleaseProject,
            ReleaseProjectEnvironment,
        )

        try:
            with transaction.atomic():
                self.update(organization=organization)
        except IntegrityError:
            # likely this means a team already exists, let's try to coerce to
            # it instead of a blind transfer
            new_team = Team.objects.get(organization=organization,
                                        slug=self.slug)
        else:
            new_team = self

        project_ids = list(
            Project.objects.filter(teams=self).exclude(
                organization=organization).values_list("id", flat=True))

        # remove associations with releases from other org
        ReleaseProject.objects.filter(project_id__in=project_ids).delete()
        ReleaseProjectEnvironment.objects.filter(
            project_id__in=project_ids).delete()

        Project.objects.filter(id__in=project_ids).update(
            organization=organization)

        ProjectTeam.objects.filter(project_id__in=project_ids).update(
            team=new_team)

        # remove any pending access requests from the old organization
        if self != new_team:
            OrganizationAccessRequest.objects.filter(team=self).delete()

        # identify shared members and ensure they retain team access
        # under the new organization
        old_memberships = OrganizationMember.objects.filter(
            teams=self).exclude(organization=organization)
        for member in old_memberships:
            try:
                new_member = OrganizationMember.objects.get(
                    user=member.user, organization=organization)
            except OrganizationMember.DoesNotExist:
                continue

            try:
                with transaction.atomic():
                    OrganizationMemberTeam.objects.create(
                        team=new_team, organizationmember=new_member)
            except IntegrityError:
                pass

        OrganizationMemberTeam.objects.filter(team=self).exclude(
            organizationmember__organization=organization).delete()

        if new_team != self:
            cursor = connections[router.db_for_write(Team)].cursor()
            # we use a cursor here to avoid automatic cascading of relations
            # in Django
            try:
                cursor.execute("DELETE FROM sentry_team WHERE id = %s",
                               [self.id])
            finally:
                cursor.close()

    def get_audit_log_data(self):
        return {
            "id": self.id,
            "slug": self.slug,
            "name": self.name,
            "status": self.status
        }

    def get_projects(self):
        from sentry.models import Project

        return Project.objects.get_for_team_ids({self.id})

    def delete(self, **kwargs):
        from sentry.models import ExternalActor

        # There is no foreign key relationship so we have to manually delete the ExternalActors
        ExternalActor.objects.filter(actor_id=self.actor_id).delete()
        return super().delete(**kwargs)
Пример #28
0
class Rule(Model):
    __core__ = True

    DEFAULT_CONDITION_MATCH = "all"  # any, all
    DEFAULT_FILTER_MATCH = "all"  # match to apply on filters
    DEFAULT_FREQUENCY = 30  # minutes

    project = FlexibleForeignKey("sentry.Project")
    environment_id = BoundedPositiveIntegerField(null=True)
    label = models.CharField(max_length=64)
    data = GzippedDictField()
    status = BoundedPositiveIntegerField(
        default=RuleStatus.ACTIVE,
        choices=((RuleStatus.ACTIVE, "Active"), (RuleStatus.INACTIVE,
                                                 "Inactive")),
        db_index=True,
    )

    date_added = models.DateTimeField(default=timezone.now)

    objects = BaseManager(cache_fields=("pk", ))

    class Meta:
        db_table = "sentry_rule"
        app_label = "sentry"

    __repr__ = sane_repr("project_id", "label")

    @classmethod
    def get_for_project(cls, project_id):
        cache_key = "project:{}:rules".format(project_id)
        rules_list = cache.get(cache_key)
        if rules_list is None:
            rules_list = list(
                cls.objects.filter(project=project_id,
                                   status=RuleStatus.ACTIVE))
            cache.set(cache_key, rules_list, 60)
        return rules_list

    @property
    def created_by(self):
        try:
            created_activity = RuleActivity.objects.get(
                rule=self, type=RuleActivityType.CREATED.value)
            return created_activity.user
        except RuleActivity.DoesNotExist:
            pass

        return None

    def delete(self, *args, **kwargs):
        rv = super().delete(*args, **kwargs)
        cache_key = "project:{}:rules".format(self.project_id)
        cache.delete(cache_key)
        return rv

    def save(self, *args, **kwargs):
        rv = super().save(*args, **kwargs)
        cache_key = "project:{}:rules".format(self.project_id)
        cache.delete(cache_key)
        return rv

    def get_audit_log_data(self):
        return {"label": self.label, "data": self.data, "status": self.status}
Пример #29
0
class GroupTagValue(Model):
    """
    Stores the total number of messages seen by a group matching
    the given filter.
    """
    __core__ = False

    project_id = BoundedBigIntegerField(db_index=True)
    group_id = BoundedBigIntegerField(db_index=True)
    times_seen = BoundedPositiveIntegerField(default=0)
    _key = FlexibleForeignKey('tagstore.TagKey', db_column='key_id')
    _value = FlexibleForeignKey('tagstore.TagValue', db_column='value_id')
    last_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)
    first_seen = models.DateTimeField(
        default=timezone.now, db_index=True, null=True)

    objects = TagStoreManager(select_related=('_value', '_key'))

    class Meta:
        app_label = 'tagstore'
        unique_together = (('project_id', 'group_id', '_key', '_value'), )
        index_together = (('project_id', '_key', '_value', 'last_seen'), )

    __repr__ = sane_repr('project_id', 'group_id', '_key', '_value')

    def delete_for_merge(self):
        using = router.db_for_read(GroupTagValue)
        cursor = connections[using].cursor()
        cursor.execute(
            """
            DELETE FROM tagstore_grouptagvalue
            WHERE project_id = %s
              AND id = %s
        """, [self.project_id, self.id]
        )

    @property
    def key(self):
        if hasattr(self, '_set_key'):
            return self._set_key

        if hasattr(self, '__key_cache'):
            return self._key.key

        # fallback
        from sentry.tagstore.v2.models import TagKey

        tk = TagKey.objects.filter(
            project_id=self.project_id,
            id=self._key_id,
        ).values_list('key', flat=True).get()

        # cache for future calls
        self.key = tk

        return tk

    @key.setter
    def key(self, key):
        self._set_key = key

    @property
    def value(self):
        if hasattr(self, '_set_value'):
            return self._set_value

        if hasattr(self, '__value_cache'):
            return self._value.value

        # fallback
        from sentry.tagstore.v2.models import TagValue

        tv = TagValue.objects.filter(
            project_id=self.project_id,
            id=self._value_id,
        ).values_list('value', flat=True).get()

        # cache for future calls
        self.value = tv

        return tv

    @value.setter
    def value(self, value):
        self._set_value = value

    def save(self, *args, **kwargs):
        if not self.first_seen:
            self.first_seen = self.last_seen
        super(GroupTagValue, self).save(*args, **kwargs)

    def merge_counts(self, new_group):
        try:
            with transaction.atomic(using=router.db_for_write(GroupTagValue)):
                new_obj = GroupTagValue.objects.get(
                    group_id=new_group.id,
                    project_id=new_group.project_id,
                    _key_id=self._key_id,
                    _value_id=self._value_id,
                )

                GroupTagValue.objects.filter(
                    id=new_obj.id,
                    project_id=new_group.project_id,
                ).update(
                    first_seen=min(new_obj.first_seen, self.first_seen),
                    last_seen=max(new_obj.last_seen, self.last_seen),
                    times_seen=new_obj.times_seen + self.times_seen,
                )
        except DataError:
            # it's possible to hit an out of range value for counters
            pass
Пример #30
0
class ProjectOwnership(Model):
    __core__ = True

    project = FlexibleForeignKey("sentry.Project", unique=True)
    raw = models.TextField(null=True)
    schema = JSONField(null=True)
    fallthrough = models.BooleanField(default=True)
    auto_assignment = models.BooleanField(default=False)
    date_created = models.DateTimeField(default=timezone.now)
    last_updated = models.DateTimeField(default=timezone.now)
    is_active = models.BooleanField(default=True)

    # An object to indicate ownership is implicitly everyone
    Everyone = object()

    class Meta:
        app_label = "sentry"
        db_table = "sentry_projectownership"

    __repr__ = sane_repr("project_id", "is_active")

    @classmethod
    def get_cache_key(self, project_id):
        return f"projectownership_project_id:1:{project_id}"

    @classmethod
    def get_ownership_cached(cls, project_id):
        """
        Cached read access to projectownership.

        This method implements a negative cache which saves us
        a pile of read queries in post_processing as most projects
        don't have ownership rules.

        See the post_save and post_delete signals below for additional
        cache updates.
        """
        cache_key = cls.get_cache_key(project_id)
        ownership = cache.get(cache_key)
        if ownership is None:
            try:
                ownership = cls.objects.get(project_id=project_id)
            except cls.DoesNotExist:
                ownership = False
            cache.set(cache_key, ownership, READ_CACHE_DURATION)
        return ownership or None

    @classmethod
    def get_owners(cls, project_id, data):
        """
        For a given project_id, and event data blob.

        If Everyone is returned, this means we implicitly are
        falling through our rules and everyone is responsible.

        If an empty list is returned, this means there are explicitly
        no owners.
        """
        ownership = cls.get_ownership_cached(project_id)
        if not ownership:
            ownership = cls(project_id=project_id)

        rules = cls._matching_ownership_rules(ownership, project_id, data)
        if not rules:
            return cls.Everyone if ownership.fallthrough else [], None

        owners = {o for rule in rules for o in rule.owners}
        owners_to_actors = resolve_actors(owners, project_id)
        ordered_actors = []
        for rule in rules:
            for o in rule.owners:
                if o in owners and owners_to_actors.get(o) is not None:
                    ordered_actors.append(owners_to_actors[o])
                    owners.remove(o)

        return ordered_actors, rules

    @classmethod
    def get_autoassign_owners(cls, project_id, data, limit=2):
        """
        Get the auto-assign owner for a project if there are any.

        Returns a tuple of (auto_assignment_enabled, list_of_owners).
        """
        with metrics.timer("projectownership.get_autoassign_owners"):
            ownership = cls.get_ownership_cached(project_id)
            if not ownership:
                return False, []

            rules = cls._matching_ownership_rules(ownership, project_id, data)
            if not rules:
                return ownership.auto_assignment, []

            # We want the last matching rule to take the most precedence.
            owners = [owner for rule in rules for owner in rule.owners]
            owners.reverse()
            actors = {
                key: val
                for key, val in resolve_actors({owner
                                                for owner in owners},
                                               project_id).items() if val
            }
            actors = [actors[owner] for owner in owners
                      if owner in actors][:limit]

            # Can happen if the ownership rule references a user/team that no longer
            # is assigned to the project or has been removed from the org.
            if not actors:
                return ownership.auto_assignment, []

            from sentry.models import ActorTuple

            return ownership.auto_assignment, ActorTuple.resolve_many(actors)

    @classmethod
    def _matching_ownership_rules(cls, ownership, project_id, data):
        rules = []
        if ownership.schema is not None:
            for rule in load_schema(ownership.schema):
                if rule.test(data):
                    rules.append(rule)

        return rules