Ejemplo n.º 1
0
class Activity(Model):
    __core__ = False

    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
    RELEASE = 10
    ASSIGNED = 11
    UNASSIGNED = 12

    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'),
        (RELEASE, 'release'),
        (ASSIGNED, 'assigned'),
        (UNASSIGNED, 'unassigned'),
    )

    project = FlexibleForeignKey('sentry.Project')
    group = FlexibleForeignKey('sentry.Group', null=True)
    event = FlexibleForeignKey('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 = FlexibleForeignKey(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 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)

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

    def get_recipients(self):
        from sentry.models import UserOption

        if self.type == Activity.ASSIGNED:
            # dont email the user if they took the action
            send_to = [self.data['assignee']]

        else:
            member_set = self.project.member_set.values_list('user', flat=True)

            if not member_set:
                return []

            disabled = set(
                UserOption.objects.filter(
                    user__in=member_set,
                    key='subscribe_notes',
                    value=u'0',
                ).values_list('user', flat=True))

            send_to = [u for u in member_set if u not in disabled]

        # never include the actor
        send_to = [u for u in send_to if u != self.user_id]

        return send_to

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

        if self.type not in (Activity.NOTE, Activity.ASSIGNED,
                             Activity.RELEASE):
            return

        send_to = self.get_recipients()

        if not send_to:
            return

        project = self.project
        org = self.project.organization

        if self.user:
            author = self.user.first_name or self.user.username
        else:
            author = None

        subject_prefix = self.project.get_option('subject_prefix',
                                                 settings.EMAIL_SUBJECT_PREFIX)
        if subject_prefix:
            subject_prefix = subject_prefix.rstrip() + ' '

        if self.group:
            subject = '%s%s' % (subject_prefix, self.group.get_email_subject())
        elif self.type == Activity.RELEASE:
            subject = '%sRelease %s' % (subject_prefix, self.data['version'])
        else:
            raise NotImplementedError

        headers = {}

        context = {
            'data':
            self.data,
            'author':
            author,
            'project':
            self.project,
            'project_link':
            absolute_uri(
                reverse('sentry-stream',
                        kwargs={
                            'organization_slug': org.slug,
                            'project_id': project.slug,
                        })),
        }

        if self.group:
            headers.update({
                'X-Sentry-Reply-To':
                group_id_to_email(self.group.id),
            })

            context.update({
                'group': self.group,
                'link': self.group.get_absolute_url(),
            })

        # TODO(dcramer): abstract each activity email into its own helper class
        if self.type == Activity.RELEASE:
            context.update({
                'release':
                Release.objects.get(
                    version=self.data['version'],
                    project=project,
                ),
                'release_link':
                absolute_uri(
                    reverse('sentry-release-details',
                            kwargs={
                                'organization_slug': org.slug,
                                'project_id': project.slug,
                                'version': self.data['version'],
                            })),
            })

        template_name = self.get_type_display()

        msg = MessageBuilder(
            subject=subject,
            context=context,
            template='sentry/emails/activity/{}.txt'.format(template_name),
            html_template='sentry/emails/activity/{}.html'.format(
                template_name),
            headers=headers,
            reference=self,
            reply_reference=self.group,
        )
        msg.add_users(send_to, project=self.project)
        msg.send_async()
Ejemplo n.º 2
0
class AlertRuleTriggerAction(Model):
    """
    This model represents an action that occurs when a trigger is fired. This is
    typically some sort of notification.
    """

    __core__ = True

    _type_registrations = {}

    # Which sort of action to take
    class Type(Enum):
        EMAIL = 0
        PAGERDUTY = 1
        SLACK = 2

    class TargetType(Enum):
        # A direct reference, like an email address, Slack channel or PagerDuty service
        SPECIFIC = 0
        # A specific user. This could be used to grab the user's email address.
        USER = 1
        # A specific team. This could be used to send an email to everyone associated
        # with a team.
        TEAM = 2

    TypeRegistration = namedtuple(
        "TypeRegistration",
        [
            "handler", "slug", "type", "supported_target_types",
            "integration_provider"
        ],
    )

    alert_rule_trigger = FlexibleForeignKey("sentry.AlertRuleTrigger")
    integration = FlexibleForeignKey("sentry.Integration", null=True)
    type = models.SmallIntegerField()
    target_type = models.SmallIntegerField()
    # Identifier used to perform the action on a given target
    target_identifier = models.TextField(null=True)
    # Human readable name to display in the UI
    target_display = models.TextField(null=True)
    date_added = models.DateTimeField(default=timezone.now)

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

    @property
    def target(self):
        if self.target_type == self.TargetType.USER.value:
            try:
                return User.objects.get(id=int(self.target_identifier))
            except User.DoesNotExist:
                pass
        elif self.target_type == self.TargetType.TEAM.value:
            try:
                return Team.objects.get(id=int(self.target_identifier))
            except Team.DoesNotExist:
                pass
        elif self.target_type == self.TargetType.SPECIFIC.value:
            # TODO: This is only for email. We should have a way of validating that it's
            # ok to contact this email.
            return self.target_identifier

    def build_handler(self, incident, project):
        type = AlertRuleTriggerAction.Type(self.type)
        if type in self._type_registrations:
            return self._type_registrations[type].handler(
                self, incident, project)
        else:
            metrics.incr("alert_rule_trigger.unhandled_type.{}".format(
                self.type))

    def fire(self, incident, project, metric_value):
        handler = self.build_handler(incident, project)
        if handler:
            return handler.fire(metric_value)

    def resolve(self, incident, project, metric_value):
        handler = self.build_handler(incident, project)
        if handler:
            return handler.resolve(metric_value)

    @classmethod
    def register_type(cls,
                      slug,
                      type,
                      supported_target_types,
                      integration_provider=None):
        """
        Registers a handler for a given type.
        :param slug: A string representing the name of this type registration
        :param type: The `Type` to handle.
        :param handler: A subclass of `ActionHandler` that accepts the
        `AlertRuleTriggerAction` and `Incident`.
        :param integration_provider: String representing the integration provider
        related to this type.
        """
        def inner(handler):
            if type not in cls._type_registrations:
                cls._type_registrations[type] = cls.TypeRegistration(
                    handler, slug, type, frozenset(supported_target_types),
                    integration_provider)
            else:
                raise Exception(u"Handler already registered for type %s" %
                                type)
            return handler

        return inner

    @classmethod
    def get_registered_type(cls, type):
        return cls._type_registrations[type]

    @classmethod
    def get_registered_types(cls):
        return cls._type_registrations.values()
Ejemplo n.º 3
0
class Event(Model):
    """
    An individual event.
    """
    __core__ = False

    group = FlexibleForeignKey('sentry.Group',
                               blank=True,
                               null=True,
                               related_name="event_set")
    event_id = models.CharField(max_length=32,
                                null=True,
                                db_column="message_id")
    project = FlexibleForeignKey('sentry.Project', null=True)
    message = models.TextField()
    num_comments = BoundedPositiveIntegerField(default=0, null=True)
    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.group_id or x.group.id)

    objects = BaseManager()

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

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

    def error(self):
        message = strip(self.message)
        if not message:
            message = '<unlabeled message>'
        else:
            message = truncatechars(message.splitlines()[0], 100)
        return 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 message_short(self):
        message = strip(self.message)
        if not message:
            message = '<unlabeled message>'
        else:
            message = truncatechars(message.splitlines()[0], 100)
        return message

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

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

    @property
    def version(self):
        return self.data.get('version', '5')

    @memoize
    def ip_address(self):
        user_data = self.data.get('sentry.interfaces.User',
                                  self.data.get('user'))
        if user_data:
            value = user_data.get('ip_address')
            if value:
                return value

        http_data = self.data.get('sentry.interfaces.Http',
                                  self.data.get('http'))
        if http_data and 'env' in http_data:
            value = http_data['env'].get('REMOTE_ADDR')
            if value:
                return value

        return None

    def get_interfaces(self):
        result = []
        for key, data in self.data.iteritems():
            try:
                cls = get_interface(key)
            except ValueError:
                continue

            value = safe_execute(cls.to_python, data)
            if not value:
                continue

            result.append((key, value))

        return OrderedDict((k, v) for k, v in sorted(
            result, key=lambda x: x[1].get_score(), reverse=True))

    @memoize
    def interfaces(self):
        return self.get_interfaces()

    def get_tags(self, with_internal=True):
        try:
            return sorted((t, v) for t, v in self.data.get('tags') or ()
                          if with_internal or not t.startswith('sentry:'))
        except ValueError:
            # at one point Sentry allowed invalid tag sets such as (foo, bar)
            # vs ((tag, foo), (tag, bar))
            return []

    tags = property(get_tags)

    def get_tag(self, key):
        for t, v in (self.data.get('tags') or ()):
            if t == key:
                return v
        return None

    def as_dict(self):
        # We use a OrderedDict to keep elements ordered for a potential JSON serializer
        data = OrderedDict()
        data['id'] = self.event_id
        data['project'] = self.project_id
        data['release'] = self.get_tag('sentry:release')
        data['platform'] = self.platform
        data['culprit'] = self.group.culprit
        data['message'] = self.message
        data['datetime'] = self.datetime
        data['time_spent'] = self.time_spent
        data['tags'] = self.get_tags()
        for k, v in sorted(self.data.iteritems()):
            data[k] = v
        return data

    @property
    def size(self):
        data_len = len(self.message)
        for value in self.data.itervalues():
            data_len += len(repr(value))
        return data_len

    # XXX(dcramer): compatibility with plugins
    def get_level_display(self):
        warnings.warn(
            'Event.get_level_display is deprecated. Use Event.tags instead.',
            DeprecationWarning)
        return self.group.get_level_display()

    @property
    def level(self):
        warnings.warn('Event.level is deprecated. Use Event.tags instead.',
                      DeprecationWarning)
        return self.group.level

    @property
    def logger(self):
        warnings.warn('Event.logger is deprecated. Use Event.tags instead.',
                      DeprecationWarning)
        return self.get_tag('logger')

    @property
    def site(self):
        warnings.warn('Event.site is deprecated. Use Event.tags instead.',
                      DeprecationWarning)
        return self.get_tag('site')

    @property
    def server_name(self):
        warnings.warn(
            'Event.server_name is deprecated. Use Event.tags instead.')
        return self.get_tag('server_name')

    @property
    def culprit(self):
        warnings.warn(
            'Event.culprit is deprecated. Use Group.culprit instead.')
        return self.group.culprit

    @property
    def checksum(self):
        warnings.warn('Event.checksum is no longer used', DeprecationWarning)
        return ''
Ejemplo n.º 4
0
class Identity(Model):
    """
    A verified link between a user and a third party identity.
    """

    __core__ = False

    idp = FlexibleForeignKey("sentry.IdentityProvider")
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL)
    external_id = models.TextField()
    data = EncryptedJsonField()
    status = BoundedPositiveIntegerField(default=IdentityStatus.UNKNOWN)
    scopes = ArrayField()
    date_verified = models.DateTimeField(default=timezone.now)
    date_added = models.DateTimeField(default=timezone.now)

    class Meta:
        app_label = "sentry"
        db_table = "sentry_identity"
        unique_together = (("idp", "external_id"), ("idp", "user"))

    def get_provider(self):
        from sentry.identity import get

        return get(self.idp.type)

    @classmethod
    def reattach(cls, idp, external_id, user, defaults):
        """
        Removes identities under `idp` associated with either `external_id` or `user`
        and creates a new identity linking them.
        """
        lookup = Q(external_id=external_id) | Q(user=user)
        Identity.objects.filter(lookup, idp=idp).delete()
        logger.info(
            "deleted-identity",
            extra={
                "external_id": external_id,
                "idp_id": idp.id,
                "user_id": user.id
            },
        )

        identity_model = Identity.objects.create(idp=idp,
                                                 user=user,
                                                 external_id=external_id,
                                                 **defaults)
        logger.info(
            "created-identity",
            extra={
                "idp_id": idp.id,
                "external_id": external_id,
                "object_id": identity_model.id,
                "user_id": user.id,
            },
        )
        return identity_model

    @classmethod
    def update_external_id_and_defaults(cls, idp, external_id, user, defaults):
        """
        Updates the identity object for a given user and identity provider
        with the new external id and other fields related to the identity status
        """
        query = Identity.objects.filter(user=user, idp=idp)
        query.update(external_id=external_id, **defaults)
        identity_model = query.first()
        logger.info(
            "updated-identity",
            extra={
                "external_id": external_id,
                "idp_id": idp.id,
                "user_id": user.id,
                "identity_id": identity_model.id,
            },
        )
        return identity_model
Ejemplo n.º 5
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"),
            (NotificationSettingTypes.APPROVAL, "approval"),
            (NotificationSettingTypes.QUOTA, "quota"),
            (NotificationSettingTypes.QUOTA_ERRORS, "quotaErrors"),
            (NotificationSettingTypes.QUOTA_TRANSACTIONS, "quotaTransactions"),
            (NotificationSettingTypes.QUOTA_ATTACHMENTS, "quotaAttacments"),
            (NotificationSettingTypes.QUOTA_WARNINGS, "quotaWarnings"),
        ),
        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",
    )
Ejemplo n.º 6
0
class ProjectKey(Model):
    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)
    user = FlexibleForeignKey(settings.AUTH_USER_MODEL, 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)

    # For audits
    user_added = FlexibleForeignKey(settings.AUTH_USER_MODEL,
                                    null=True,
                                    related_name='keys_added_set')
    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:delete',
        'event:read',
        'event:write',
        'event:delete',
    )

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

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

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

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

    @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()
        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

        urlparts = urlparse(url or settings.SENTRY_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)

    def get_audit_log_data(self):
        return {
            'label': self.label,
            'user_id': self.user_id,
            'public_key': self.public_key,
            'secret_key': self.secret_key,
            'roles': int(self.roles),
            'status': self.status,
        }
Ejemplo n.º 7
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.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.SET_ONDEMAND, 'ondemand.edit'),
        (AuditLogEntryEvent.TRIAL_STARTED, 'trial.started'),
        (AuditLogEntryEvent.PLAN_CHANGED, 'plan.changed'),
    ))
    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.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' % (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.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'])

        return ''
class Release(Model):
    """
    A release is generally created when a new version is pushed into a
    production state.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project')
    version = models.CharField(max_length=64)
    # ref might be the branch name being released
    ref = models.CharField(max_length=64, null=True, blank=True)
    url = models.URLField(null=True, blank=True)
    date_added = models.DateTimeField(default=timezone.now)
    date_started = models.DateTimeField(null=True, blank=True)
    date_released = models.DateTimeField(null=True, blank=True)
    # arbitrary data recorded with the release
    data = JSONField(default={})
    new_groups = BoundedPositiveIntegerField(default=0)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_release'
        unique_together = (('project', 'version'), )

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

    @classmethod
    def get_cache_key(cls, project_id, version):
        return 'release:2:%s:%s' % (project_id, md5(version).hexdigest())

    @classmethod
    def get(cls, project, version):
        cache_key = cls.get_cache_key(project.id, version)

        release = cache.get(cache_key)
        if release is None:
            try:
                release = cls.objects.get(
                    project=project,
                    version=version,
                )
            except cls.DoesNotExist:
                release = -1
            cache.set(cache_key, release, 300)

        if release == -1:
            return

        return release

    @classmethod
    def get_or_create(cls, project, version, date_added):
        cache_key = cls.get_cache_key(project.id, version)

        release = cache.get(cache_key)
        if release in (None, -1):
            # TODO(dcramer): if the cache result is -1 we could attempt a
            # default create here instead of default get
            release = cls.objects.get_or_create(
                project=project,
                version=version,
                defaults={
                    'date_added': date_added,
                },
            )[0]
            cache.set(cache_key, release, 3600)

        return release
Ejemplo n.º 9
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 = BaseManager(cache_fields=('public_key', 'secret_key', ))

    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 __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

    @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(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 = self.get_endpoint()

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

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

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

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

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

    @property
    def js_sdk_loader_cdn_url(self):
        if settings.JS_SDK_LOADER_CDN_URL:
            return '%s%s.min.js' % (settings.JS_SDK_LOADER_CDN_URL, self.public_key)
        else:
            endpoint = self.get_endpoint()
            return '%s%s' % (
                endpoint,
                reverse('sentry-js-sdk-loader', args=[self.public_key, '.min'])
            )

    def get_endpoint(self):
        endpoint = settings.SENTRY_PUBLIC_ENDPOINT or settings.SENTRY_ENDPOINT
        if not endpoint:
            endpoint = options.get('system.url-prefix')
        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
Ejemplo n.º 10
0
class ApiToken(Model):
    __core__ = True

    # users can generate tokens without being application-bound
    application = FlexibleForeignKey('sentry.ApiApplication', null=True)
    user = FlexibleForeignKey('sentry.User')
    token = models.CharField(
        max_length=64,
        unique=True,
        default=lambda: ApiToken.generate_token(),
    )
    refresh_token = models.CharField(
        max_length=64,
        unique=True,
        null=True,
        default=lambda: ApiToken.generate_token(),
    )
    expires_at = models.DateTimeField(
        null=True, default=lambda: timezone.now() + DEFAULT_EXPIRATION)
    scopes = BitField(flags=ApiScopes().to_bitfield())
    scope_list = ArrayField(of=models.TextField)
    date_added = models.DateTimeField(default=timezone.now)

    objects = BaseManager(cache_fields=('token', ))

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

    __repr__ = sane_repr('user_id', 'token', 'application_id')

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

    @classmethod
    def generate_token(cls):
        return uuid4().hex + uuid4().hex

    @classmethod
    def from_grant(cls, grant):
        with transaction.atomic():
            return cls.objects.create(
                application=grant.application,
                user=grant.user,
                scope_list=grant.get_scopes(),
            )

    def is_expired(self):
        if not self.expires_at:
            return False

        return timezone.now() >= self.expires_at

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

    def get_scopes(self):
        if self.scope_list:
            return self.scope_list
        return [k for k, v in six.iteritems(self.scopes) if v]

    def has_scope(self, scope):
        return scope in self.get_scopes()

    def get_allowed_origins(self):
        if self.application:
            return self.application.get_allowed_origins()
        return ()

    def refresh(self, expires_at=None):
        if expires_at is None:
            expires_at = timezone.now() + DEFAULT_EXPIRATION

        self.update(
            token=type(self).generate_token(),
            refresh_token=type(self).generate_token(),
            expires_at=expires_at,
        )
Ejemplo n.º 11
0
class AuditLogEntry(Model):
    __core__ = False

    organization = FlexibleForeignKey("sentry.Organization")
    actor_label = models.CharField(max_length=MAX_ACTOR_LABEL_LENGTH,
                                   null=True,
                                   blank=True)
    # if the entry was created via a user
    actor = FlexibleForeignKey("sentry.User",
                               related_name="audit_actors",
                               null=True,
                               blank=True,
                               on_delete=models.SET_NULL)
    # 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",
        on_delete=models.SET_NULL,
    )
    # 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, "servicehook.create"),
        (AuditLogEntryEvent.SERVICEHOOK_EDIT, "servicehook.edit"),
        (AuditLogEntryEvent.SERVICEHOOK_REMOVE, "servicehook.remove"),
        (AuditLogEntryEvent.SERVICEHOOK_ENABLE, "servicehook.enable"),
        (AuditLogEntryEvent.SERVICEHOOK_DISABLE, "servicehook.disable"),
        (AuditLogEntryEvent.INTEGRATION_UPGRADE, "integration.upgrade"),
        (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.INTERNAL_INTEGRATION_ADD_TOKEN,
         "internal-integration.add-token"),
        (
            AuditLogEntryEvent.INTERNAL_INTEGRATION_REMOVE_TOKEN,
            "internal-integration.remove-token",
        ),
        (AuditLogEntryEvent.SET_ONDEMAND, "ondemand.edit"),
        (AuditLogEntryEvent.TRIAL_STARTED, "trial.started"),
        (AuditLogEntryEvent.PLAN_CHANGED, "plan.changed"),
        (AuditLogEntryEvent.PLAN_CANCELLED, "plan.cancelled"),
        (AuditLogEntryEvent.INVITE_REQUEST_ADD, "invite-request.create"),
        (AuditLogEntryEvent.INVITE_REQUEST_REMOVE, "invite-request.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 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
        # trim label to the max length
        self.actor_label = self.actor_label[:MAX_ACTOR_LABEL_LENGTH]
        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(
                "{} {}".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 in [
                AuditLogEntryEvent.PROJECT_ENABLE,
                AuditLogEntryEvent.PROJECT_DISABLE
        ]:
            verb = "enabled" if self.event == AuditLogEntryEvent.PROJECT_ENABLE else "disabled"

            # Most logs will just be name of the filter, but legacy browser changes can be bool, str or sets
            filter_name = self.data["state"]
            if (filter_name in ("0", "1") or isinstance(filter_name, set)
                    or isinstance(filter_name, bool)):
                message = "%s project filter legacy-browsers" % (verb, )
                if isinstance(filter_name, set):
                    message += ": %s" % (", ".join(filter_name), )
                return message
            else:
                return "%s project filter %s" % (verb, filter_name)

        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(
                "{} {}".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:
            if self.data.get("provider"):
                return "installed %s for the %s integration" % (
                    self.data["name"],
                    self.data["provider"],
                )
            else:
                return "enabled integration %s for project %s" % (
                    self.data["integration"],
                    self.data["project"],
                )
        elif self.event == AuditLogEntryEvent.INTEGRATION_EDIT:
            if self.data.get("provider"):
                return "edited the %s for the %s integration" % (
                    self.data["name"],
                    self.data["provider"],
                )
            return "edited integration %s for project %s" % (
                self.data["integration"],
                self.data["project"],
            )
        elif self.event == AuditLogEntryEvent.INTEGRATION_REMOVE:
            if self.data.get("provider"):
                return "uninstalled %s for the %s integration" % (
                    self.data["name"],
                    self.data["provider"],
                )
            return "disabled integration %s from project %s" % (
                self.data["integration"],
                self.data["project"],
            )
        elif self.event == AuditLogEntryEvent.INTEGRATION_UPGRADE:
            if self.data.get("provider"):
                return "upgraded %s for the %s integration" % (
                    self.data["name"],
                    self.data["provider"],
                )

        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"])
        elif self.event == AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD:
            return "created internal integration %s" % (self.data.get(
                "name", ""))
        elif self.event == AuditLogEntryEvent.INTERNAL_INTEGRATION_ADD_TOKEN:
            return "created a token for internal integration %s" % (
                self.data["sentry_app"])
        elif self.event == AuditLogEntryEvent.INTERNAL_INTEGRATION_REMOVE_TOKEN:
            return "revoked a token for internal integration %s" % (
                self.data["sentry_app"])
        elif self.event == AuditLogEntryEvent.INVITE_REQUEST_ADD:
            return "request added to invite %s" % (self.data["email"], )
        elif self.event == AuditLogEntryEvent.INVITE_REQUEST_REMOVE:
            return "removed the invite request for %s" % (self.data["email"], )

        return ""
Ejemplo n.º 12
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 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):
        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 transfer_to(self, team):
        from sentry.models import ProjectTeam, ReleaseProject

        organization = team.organization
        from_team_id = self.team_id

        # 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,
            )

        ProjectTeam.objects.filter(project=self,
                                   team_id=from_team_id).update(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 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
Ejemplo n.º 13
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,
                                       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)
    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)
        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 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_muted(self):
        return self.get_status() == GroupStatus.MUTED

    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

        if self.status == GroupStatus.MUTED:
            try:
                snooze = GroupSnooze.objects.get(group=self)
            except GroupSnooze.DoesNotExist:
                pass
            else:
                # XXX(dcramer): if the snooze row exists then we need
                # to confirm its still valid
                if snooze.until > timezone.now():
                    return GroupStatus.MUTED
                else:
                    return GroupStatus.UNRESOLVED

        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):
        if not share_id:
            raise cls.DoesNotExist
        try:
            project_id, group_id = b16decode(share_id.upper()).split('.')
        except (ValueError, TypeError):
            raise cls.DoesNotExist
        if not (project_id.isdigit() and group_id.isdigit()):
            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'):
            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_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_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' % (
            self.project.get_full_name().encode('utf-8'),
            six.text_type(self.get_level_display()).upper().encode('utf-8'),
            self.message_short.encode('utf-8'))
Ejemplo n.º 14
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 = u"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(Rule, self).delete(*args, **kwargs)
        cache_key = u"project:{}:rules".format(self.project_id)
        cache.delete(cache_key)
        return rv

    def save(self, *args, **kwargs):
        rv = super(Rule, self).save(*args, **kwargs)
        cache_key = u"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}
Ejemplo n.º 15
0
class Release(Model):
    """
    A release is generally created when a new version is pushed into a
    production state.
    """
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    projects = models.ManyToManyField('sentry.Project',
                                      related_name='releases',
                                      through=ReleaseProject)
    project_id = BoundedPositiveIntegerField(null=True)
    version = models.CharField(max_length=64)
    # ref might be the branch name being released
    ref = models.CharField(max_length=64, null=True, blank=True)
    url = models.URLField(null=True, blank=True)
    date_added = models.DateTimeField(default=timezone.now)
    date_started = models.DateTimeField(null=True, blank=True)
    date_released = models.DateTimeField(null=True, blank=True)
    # arbitrary data recorded with the release
    data = JSONField(default={})
    new_groups = BoundedPositiveIntegerField(default=0)
    # generally the release manager, or the person initiating the process
    owner = FlexibleForeignKey('sentry.User', null=True, blank=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_release'
        unique_together = (('project_id', 'version'), )

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

    @classmethod
    def get_cache_key(cls, project_id, version):
        # TODO(jess): update this to use organization id when adding
        # unique on Release for organization, version
        return 'release:2:%s:%s' % (project_id, md5_text(version).hexdigest())

    @classmethod
    def get(cls, project, version):
        cache_key = cls.get_cache_key(project.id, version)

        release = cache.get(cache_key)
        if release is None:
            try:
                release = cls.objects.get(
                    organization_id=project.organization_id,
                    projects=project,
                    version=version,
                )
            except cls.DoesNotExist:
                release = -1
            cache.set(cache_key, release, 300)

        if release == -1:
            return

        return release

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

    @classmethod
    def get_or_create(cls, project, version, date_added):
        cache_key = cls.get_cache_key(project.id, version)

        release = cache.get(cache_key)
        if release in (None, -1):
            # TODO(dcramer): if the cache result is -1 we could attempt a
            # default create here instead of default get
            project_version = ('%s-%s' % (project.slug, version))[:64]
            releases = list(
                cls.objects.filter(organization_id=project.organization_id,
                                   version__in=[version, project_version],
                                   projects=project))
            if len(releases) == 1:
                release = releases[0]
            elif len(releases) > 1:
                release = [
                    r for r in releases if r.version == project_version
                ][0]
            else:
                release = cls.objects.filter(
                    organization_id=project.organization_id,
                    version=version).first()
                if not release:
                    lock_key = cls.get_lock_key(project.organization_id,
                                                version)
                    lock = locks.get(lock_key, duration=5)
                    with TimedRetryPolicy(10)(lock.acquire):
                        try:
                            release = cls.objects.get(
                                organization_id=project.organization_id,
                                version=version)
                        except cls.DoesNotExist:
                            release = cls.objects.create(
                                organization_id=project.organization_id,
                                version=version,
                                date_added=date_added)
                release.add_project(project)

            # TODO(dcramer): upon creating a new release, check if it should be
            # the new "latest release" for this project
            cache.set(cache_key, release, 3600)

        return release

    @classmethod
    def merge(cls, to_release, from_releases):
        # The following models reference release:
        # ReleaseCommit.release
        # ReleaseEnvironment.release_id
        # ReleaseProject.release
        # GroupRelease.release_id
        # GroupResolution.release
        # Group.first_release
        # ReleaseFile.release

        from sentry.models import (ReleaseCommit, ReleaseEnvironment,
                                   ReleaseFile, ReleaseProject, Group,
                                   GroupRelease, GroupResolution)

        model_list = (ReleaseCommit, ReleaseEnvironment, ReleaseFile,
                      ReleaseProject, GroupRelease, GroupResolution)
        for release in from_releases:
            for model in model_list:
                if hasattr(model, 'release'):
                    update_kwargs = {'release': to_release}
                else:
                    update_kwargs = {'release_id': to_release.id}
                try:
                    with transaction.atomic():
                        model.objects.filter(release_id=release.id).update(
                            **update_kwargs)
                except IntegrityError:
                    for item in model.objects.filter(release_id=release.id):
                        try:
                            with transaction.atomic():
                                model.objects.filter(id=item.id).update(
                                    **update_kwargs)
                        except IntegrityError:
                            item.delete()

            Group.objects.filter(first_release=release).update(
                first_release=to_release)

            release.delete()

    @property
    def short_version(self):
        if _sha1_re.match(self.version):
            return self.version[:12]
        return self.version

    def add_project(self, project):
        try:
            with transaction.atomic():
                ReleaseProject.objects.create(project=project, release=self)
        except IntegrityError:
            pass
Ejemplo n.º 16
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

    TYPE = (
        # (TYPE, verb-slug)
        (SET_RESOLVED, 'set_resolved'),
        (SET_RESOLVED_BY_AGE, 'set_resolved_by_age'),
        (SET_RESOLVED_IN_RELEASE, 'set_resolved_in_release'),
        (SET_RESOLVED_IN_COMMIT, 'set_resolved_in_commit'),
        (SET_RESOLVED_IN_PULL_REQUEST, 'set_resolved_in_pull_request'),
        (SET_UNRESOLVED, 'set_unresolved'),
        (SET_IGNORED, 'set_ignored'),
        (SET_PUBLIC, 'set_public'),
        (SET_PRIVATE, 'set_private'),
        (SET_REGRESSION, 'set_regression'),
        (CREATE_ISSUE, 'create_issue'),
        (NOTE, 'note'),
        (FIRST_SEEN, 'first_seen'),
        (RELEASE, 'release'),
        (ASSIGNED, 'assigned'),
        (UNASSIGNED, 'unassigned'),
        (MERGE, 'merge'),
        (DEPLOY, 'deploy'),
        (NEW_PROCESSING_ISSUES, 'new_processing_issues'),
        (UNMERGE_SOURCE, 'unmerge_source'),
        (UNMERGE_DESTINATION, 'unmerge_destination'),
    )

    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)
    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)
Ejemplo n.º 17
0
class Release(Model):
    """
    A release is generally created when a new version is pushed into a
    production state.
    """
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    projects = models.ManyToManyField('sentry.Project',
                                      related_name='releases',
                                      through=ReleaseProject)
    project_id = BoundedPositiveIntegerField(null=True)
    version = models.CharField(max_length=64)
    # ref might be the branch name being released
    ref = models.CharField(max_length=64, null=True, blank=True)
    url = models.URLField(null=True, blank=True)
    date_added = models.DateTimeField(default=timezone.now)
    # DEPRECATED - not available in UI or editable from API
    date_started = models.DateTimeField(null=True, blank=True)
    date_released = models.DateTimeField(null=True, blank=True)
    # arbitrary data recorded with the release
    data = JSONField(default={})
    new_groups = BoundedPositiveIntegerField(default=0)
    # generally the release manager, or the person initiating the process
    owner = FlexibleForeignKey('sentry.User', null=True, blank=True)

    # materialized stats
    commit_count = BoundedPositiveIntegerField(null=True)
    last_commit_id = BoundedPositiveIntegerField(null=True)
    authors = ArrayField(null=True)
    total_deploys = BoundedPositiveIntegerField(null=True)
    last_deploy_id = BoundedPositiveIntegerField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_release'
        unique_together = (('organization', 'version'), )

    __repr__ = sane_repr('organization', 'version')

    @staticmethod
    def is_valid_version(value):
        return not (any(c in value for c in BAD_RELEASE_CHARS)
                    or value in ('.', '..') or not value)

    @classmethod
    def get_cache_key(cls, organization_id, version):
        return 'release:3:%s:%s' % (organization_id,
                                    md5_text(version).hexdigest())

    @classmethod
    def get(cls, project, version):
        cache_key = cls.get_cache_key(project.organization_id, version)

        release = cache.get(cache_key)
        if release is None:
            try:
                release = cls.objects.get(
                    organization_id=project.organization_id,
                    projects=project,
                    version=version,
                )
            except cls.DoesNotExist:
                release = -1
            cache.set(cache_key, release, 300)

        if release == -1:
            return

        return release

    @classmethod
    def get_or_create(cls, project, version, date_added=None):
        from sentry.models import Project

        if date_added is None:
            date_added = timezone.now()

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

        release = cache.get(cache_key)
        if release in (None, -1):
            # TODO(dcramer): if the cache result is -1 we could attempt a
            # default create here instead of default get
            project_version = ('%s-%s' % (project.slug, version))[:64]
            releases = list(
                cls.objects.filter(organization_id=project.organization_id,
                                   version__in=[version, project_version],
                                   projects=project))
            if releases:
                try:
                    release = [
                        r for r in releases if r.version == project_version
                    ][0]
                except IndexError:
                    release = releases[0]
            else:
                try:
                    with transaction.atomic():
                        release = cls.objects.create(
                            organization_id=project.organization_id,
                            version=version,
                            date_added=date_added,
                            total_deploys=0,
                        )
                except IntegrityError:
                    release = cls.objects.get(
                        organization_id=project.organization_id,
                        version=version)
                release.add_project(project)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(
                        flags=F('flags').bitor(Project.flags.has_releases))

            # TODO(dcramer): upon creating a new release, check if it should be
            # the new "latest release" for this project
            cache.set(cache_key, release, 3600)

        return release

    @classmethod
    def merge(cls, to_release, from_releases):
        # The following models reference release:
        # ReleaseCommit.release
        # ReleaseEnvironment.release_id
        # ReleaseProject.release
        # GroupRelease.release_id
        # GroupResolution.release
        # Group.first_release
        # ReleaseFile.release

        from sentry.models import (ReleaseCommit, ReleaseEnvironment,
                                   ReleaseFile, ReleaseProject, Group,
                                   GroupRelease, GroupResolution)

        model_list = (ReleaseCommit, ReleaseEnvironment, ReleaseFile,
                      ReleaseProject, GroupRelease, GroupResolution)
        for release in from_releases:
            for model in model_list:
                if hasattr(model, 'release'):
                    update_kwargs = {'release': to_release}
                else:
                    update_kwargs = {'release_id': to_release.id}
                try:
                    with transaction.atomic():
                        model.objects.filter(release_id=release.id).update(
                            **update_kwargs)
                except IntegrityError:
                    for item in model.objects.filter(release_id=release.id):
                        try:
                            with transaction.atomic():
                                model.objects.filter(id=item.id).update(
                                    **update_kwargs)
                        except IntegrityError:
                            item.delete()

            Group.objects.filter(first_release=release).update(
                first_release=to_release)

            release.delete()

    @property
    def short_version(self):
        version = self.version
        match = _dotted_path_prefix_re.match(version)
        if match is not None:
            version = version[match.end():]
        if _sha1_re.match(version):
            return version[:7]
        return version

    def add_dist(self, name, date_added=None):
        from sentry.models import Distribution
        if date_added is None:
            date_added = timezone.now()
        return Distribution.objects.get_or_create(release=self,
                                                  name=name,
                                                  defaults={
                                                      'date_added':
                                                      date_added,
                                                      'organization_id':
                                                      self.organization_id,
                                                  })[0]

    def get_dist(self, name):
        from sentry.models import Distribution
        try:
            return Distribution.objects.get(name=name, release=self)
        except Distribution.DoesNotExist:
            pass

    def add_project(self, project):
        """
        Add a project to this release.

        Returns True if the project was added and did not already exist.
        """
        from sentry.models import Project
        try:
            with transaction.atomic():
                ReleaseProject.objects.create(project=project, release=self)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(flags=F('flags').bitor(
                        Project.flags.has_releases), )
        except IntegrityError:
            return False
        else:
            return True

    def set_refs(self, refs, user, fetch=False):
        from sentry.api.exceptions import InvalidRepository
        from sentry.models import Commit, ReleaseHeadCommit, Repository
        from sentry.tasks.commits import fetch_commits

        # TODO: this does the wrong thing unless you are on the most
        # recent release.  Add a timestamp compare?
        prev_release = type(self).objects.filter(
            organization_id=self.organization_id,
            projects__in=self.projects.all(),
        ).exclude(version=self.version).order_by('-date_added').first()

        names = {r['repository'] for r in refs}
        repos = list(
            Repository.objects.filter(
                organization_id=self.organization_id,
                name__in=names,
            ))
        repos_by_name = {r.name: r for r in repos}
        invalid_repos = names - set(repos_by_name.keys())
        if invalid_repos:
            raise InvalidRepository('Invalid repository names: %s' %
                                    ','.join(invalid_repos))

        for ref in refs:
            repo = repos_by_name[ref['repository']]

            commit = Commit.objects.get_or_create(
                organization_id=self.organization_id,
                repository_id=repo.id,
                key=ref['commit'],
            )[0]
            # update head commit for repo/release if exists
            ReleaseHeadCommit.objects.create_or_update(
                organization_id=self.organization_id,
                repository_id=repo.id,
                release=self,
                values={
                    'commit': commit,
                })
        if fetch:
            fetch_commits.apply_async(
                kwargs={
                    'release_id': self.id,
                    'user_id': user.id,
                    'refs': refs,
                    'prev_release_id': prev_release and prev_release.id,
                })

    def set_commits(self, commit_list):
        """
        Bind a list of commits to this release.

        These should be ordered from newest to oldest.

        This will clear any existing commit log and replace it with the given
        commits.
        """
        from sentry.models import (Commit, CommitAuthor, Group,
                                   GroupCommitResolution, GroupResolution,
                                   GroupResolutionStatus, GroupStatus,
                                   ReleaseCommit, Repository)
        from sentry.plugins.providers.repository import RepositoryProvider

        commit_list = [
            c for c in commit_list if
            not RepositoryProvider.should_ignore_commit(c.get('message', ''))
        ]

        with transaction.atomic():
            # TODO(dcramer): would be good to optimize the logic to avoid these
            # deletes but not overly important
            ReleaseCommit.objects.filter(release=self, ).delete()

            authors = {}
            repos = {}
            latest_commit = None
            for idx, data in enumerate(commit_list):
                repo_name = data.get('repository') or 'organization-{}'.format(
                    self.organization_id)
                if repo_name not in repos:
                    repos[repo_name] = repo = Repository.objects.get_or_create(
                        organization_id=self.organization_id,
                        name=repo_name,
                    )[0]
                else:
                    repo = repos[repo_name]

                author_email = data.get('author_email')
                if author_email is None and data.get('author_name'):
                    author_email = (re.sub(r'[^a-zA-Z0-9\-_\.]*', '',
                                           data['author_name']).lower() +
                                    '@localhost')

                if not author_email:
                    author = None
                elif author_email not in authors:
                    authors[
                        author_email] = author = CommitAuthor.objects.get_or_create(
                            organization_id=self.organization_id,
                            email=author_email,
                            defaults={
                                'name': data.get('author_name'),
                            })[0]
                    if data.get('author_name'
                                ) and author.name != data['author_name']:
                        author.update(name=data['author_name'])
                else:
                    author = authors[author_email]

                defaults = {
                    'message': data.get('message'),
                    'author': author,
                    'date_added': data.get('timestamp') or timezone.now(),
                }
                commit, created = Commit.objects.get_or_create(
                    organization_id=self.organization_id,
                    repository_id=repo.id,
                    key=data['id'],
                    defaults=defaults,
                )
                if not created:
                    update_kwargs = {}
                    if commit.message is None and defaults[
                            'message'] is not None:
                        update_kwargs['message'] = defaults['message']
                    if commit.author_id is None and defaults[
                            'author'] is not None:
                        update_kwargs['author'] = defaults['author']
                    if update_kwargs:
                        commit.update(**update_kwargs)

                ReleaseCommit.objects.create(
                    organization_id=self.organization_id,
                    release=self,
                    commit=commit,
                    order=idx,
                )
                if latest_commit is None:
                    latest_commit = commit

            self.update(
                commit_count=len(commit_list),
                authors=[six.text_type(a.id) for a in six.itervalues(authors)],
                last_commit_id=latest_commit.id if latest_commit else None,
            )

        group_ids = list(
            GroupCommitResolution.objects.filter(
                commit_id__in=ReleaseCommit.objects.filter(
                    release=self).values_list(
                        'commit_id', flat=True), ).values_list('group_id',
                                                               flat=True))
        for group_id in group_ids:
            GroupResolution.objects.create_or_update(
                group_id=group_id,
                release=self,
                values={
                    'status': GroupResolutionStatus.RESOLVED,
                },
            )

        if group_ids:
            Group.objects.filter(
                id__in=group_ids, ).update(status=GroupStatus.RESOLVED)
Ejemplo n.º 18
0
class Team(Model):
    """
    A team represents a group of individuals which maintain ownership of projects.
    """
    __core__ = 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)
    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 __unicode__(self):
        return u'%s (%s)' % (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(Team, self).save(*args, **kwargs)
        else:
            super(Team, self).save(*args, **kwargs)

    @property
    def member_set(self):
        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)

        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()

        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,
        }
Ejemplo n.º 19
0
class Release(Model):
    """
    A release is generally created when a new version is pushed into a
    production state.
    """
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    projects = models.ManyToManyField('sentry.Project',
                                      related_name='releases',
                                      through=ReleaseProject)
    project_id = BoundedPositiveIntegerField(null=True)
    version = models.CharField(max_length=64)
    # ref might be the branch name being released
    ref = models.CharField(max_length=64, null=True, blank=True)
    url = models.URLField(null=True, blank=True)
    date_added = models.DateTimeField(default=timezone.now)
    date_started = models.DateTimeField(null=True, blank=True)
    date_released = models.DateTimeField(null=True, blank=True)
    # arbitrary data recorded with the release
    data = JSONField(default={})
    new_groups = BoundedPositiveIntegerField(default=0)
    # generally the release manager, or the person initiating the process
    owner = FlexibleForeignKey('sentry.User', null=True, blank=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_release'
        unique_together = (('organization', 'version'), )

    __repr__ = sane_repr('organization', 'version')

    @classmethod
    def get_cache_key(cls, organization_id, version):
        return 'release:3:%s:%s' % (organization_id,
                                    md5_text(version).hexdigest())

    @classmethod
    def get(cls, project, version):
        cache_key = cls.get_cache_key(project.organization_id, version)

        release = cache.get(cache_key)
        if release is None:
            try:
                release = cls.objects.get(
                    organization_id=project.organization_id,
                    projects=project,
                    version=version,
                )
            except cls.DoesNotExist:
                release = -1
            cache.set(cache_key, release, 300)

        if release == -1:
            return

        return release

    @classmethod
    def get_or_create(cls, project, version, date_added):
        from sentry.models import Project

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

        release = cache.get(cache_key)
        if release in (None, -1):
            # TODO(dcramer): if the cache result is -1 we could attempt a
            # default create here instead of default get
            project_version = ('%s-%s' % (project.slug, version))[:64]
            releases = list(
                cls.objects.filter(organization_id=project.organization_id,
                                   version__in=[version, project_version],
                                   projects=project))
            if releases:
                try:
                    release = [
                        r for r in releases if r.version == project_version
                    ][0]
                except IndexError:
                    release = releases[0]
            else:
                try:
                    with transaction.atomic():
                        release = cls.objects.create(
                            organization_id=project.organization_id,
                            version=version,
                            date_added=date_added)
                except IntegrityError:
                    release = cls.objects.get(
                        organization_id=project.organization_id,
                        version=version)
                release.add_project(project)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(
                        flags=F('flags').bitor(Project.flags.has_releases))

            # TODO(dcramer): upon creating a new release, check if it should be
            # the new "latest release" for this project
            cache.set(cache_key, release, 3600)

        return release

    @classmethod
    def merge(cls, to_release, from_releases):
        # The following models reference release:
        # ReleaseCommit.release
        # ReleaseEnvironment.release_id
        # ReleaseProject.release
        # GroupRelease.release_id
        # GroupResolution.release
        # Group.first_release
        # ReleaseFile.release

        from sentry.models import (ReleaseCommit, ReleaseEnvironment,
                                   ReleaseFile, ReleaseProject, Group,
                                   GroupRelease, GroupResolution)

        model_list = (ReleaseCommit, ReleaseEnvironment, ReleaseFile,
                      ReleaseProject, GroupRelease, GroupResolution)
        for release in from_releases:
            for model in model_list:
                if hasattr(model, 'release'):
                    update_kwargs = {'release': to_release}
                else:
                    update_kwargs = {'release_id': to_release.id}
                try:
                    with transaction.atomic():
                        model.objects.filter(release_id=release.id).update(
                            **update_kwargs)
                except IntegrityError:
                    for item in model.objects.filter(release_id=release.id):
                        try:
                            with transaction.atomic():
                                model.objects.filter(id=item.id).update(
                                    **update_kwargs)
                        except IntegrityError:
                            item.delete()

            Group.objects.filter(first_release=release).update(
                first_release=to_release)

            release.delete()

    @property
    def short_version(self):
        if _sha1_re.match(self.version):
            return self.version[:12]
        return self.version

    def add_project(self, project):
        """
        Add a project to this release.

        Returns True if the project was added and did not already exist.
        """
        from sentry.models import Project
        try:
            with transaction.atomic():
                ReleaseProject.objects.create(project=project, release=self)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(flags=F('flags').bitor(
                        Project.flags.has_releases), )
        except IntegrityError:
            return False
        else:
            return True

    def set_commits(self, commit_list):
        from sentry.models import Commit, CommitAuthor, ReleaseCommit, Repository

        with transaction.atomic():
            # TODO(dcramer): would be good to optimize the logic to avoid these
            # deletes but not overly important
            ReleaseCommit.objects.filter(release=self, ).delete()

            authors = {}
            repos = {}
            for idx, data in enumerate(commit_list):
                repo_name = data.get('repository') or 'organization-{}'.format(
                    self.organization_id)
                if repo_name not in repos:
                    repos[repo_name] = repo = Repository.objects.get_or_create(
                        organization_id=self.organization_id,
                        name=repo_name,
                    )[0]
                else:
                    repo = repos[repo_name]

                author_email = data.get('author_email')
                if author_email is None and data.get('author_name'):
                    author_email = (re.sub(r'[^a-zA-Z0-9\-_\.]*', '',
                                           data['author_name']).lower() +
                                    '@localhost')

                if not author_email:
                    author = None
                elif author_email not in authors:
                    authors[
                        author_email] = author = CommitAuthor.objects.get_or_create(
                            organization_id=self.organization_id,
                            email=author_email,
                            defaults={
                                'name': data.get('author_name'),
                            })[0]
                    if data.get('author_name'
                                ) and author.name != data['author_name']:
                        author.update(name=data['author_name'])
                else:
                    author = authors[author_email]

                commit = Commit.objects.get_or_create(
                    organization_id=self.organization_id,
                    repository_id=repo.id,
                    key=data['id'],
                    defaults={
                        'message': data.get('message'),
                        'author': author,
                        'date_added': data.get('timestamp') or timezone.now(),
                    })[0]

                ReleaseCommit.objects.create(
                    organization_id=self.organization_id,
                    release=self,
                    commit=commit,
                    order=idx,
                )
Ejemplo n.º 20
0
class ApiKey(Model):
    __core__ = True

    organization = FlexibleForeignKey('sentry.Organization',
                                      related_name='key_set')
    label = models.CharField(max_length=64, blank=True, default='Default')
    key = models.CharField(max_length=32, unique=True)
    scopes = BitField(flags=(
        ('project:read', 'project:read'),
        ('project:write', 'project:write'),
        ('project:admin', 'project:admin'),
        ('project:releases', 'project:releases'),
        ('team:read', 'team:read'),
        ('team:write', 'team:write'),
        ('team:admin', 'team:admin'),
        ('event:read', 'event:read'),
        ('event:write', 'event:write'),
        ('event:admin', 'event:admin'),
        ('org:read', 'org:read'),
        ('org:write', 'org:write'),
        ('org:admin', 'org:admin'),
        ('member:read', 'member:read'),
        ('member:write', 'member:write'),
        ('member:admin', 'member:admin'),
    ))
    scope_list = ArrayField(of=models.TextField)
    status = BoundedPositiveIntegerField(default=0,
                                         choices=(
                                             (ApiKeyStatus.ACTIVE,
                                              _('Active')),
                                             (ApiKeyStatus.INACTIVE,
                                              _('Inactive')),
                                         ),
                                         db_index=True)
    date_added = models.DateTimeField(default=timezone.now)
    allowed_origins = models.TextField(blank=True, null=True)

    objects = BaseManager(cache_fields=('key', ))

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

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

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

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

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

    def save(self, *args, **kwargs):
        if not self.key:
            self.key = ApiKey.generate_api_key()
        super(ApiKey, self).save(*args, **kwargs)

    def get_allowed_origins(self):
        if not self.allowed_origins:
            return []
        return filter(bool, self.allowed_origins.split('\n'))

    def get_audit_log_data(self):
        return {
            'label': self.label,
            'key': self.key,
            'scopes': self.get_scopes(),
            'status': self.status,
        }

    def get_scopes(self):
        if self.scope_list:
            return self.scope_list
        return [k for k, v in six.iteritems(self.scopes) if v]

    def has_scope(self, scope):
        return scope in self.get_scopes()
Ejemplo n.º 21
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"),
            ("has_issue_alerts_targeting",
             "This Project has issue alerts targeting"),
            ("has_transactions", "This Project has sent transactions"),
            ("has_alert_filters", "This Project has filters"),
        ),
        default=10,
        null=True,
    )

    objects = ProjectManager(cache_fields=["pk"])
    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 __str__(self):
        return f"{self.name} ({self.slug})"

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

        with sentry_sdk.start_span(
                op="project.next_short_id") as span, metrics.timer(
                    "project.next_short_id"):
            span.set_data("project_id", self.id)
            span.set_data("project_slug", self.slug)
            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,
                    max_length=50,
                )
            super().save(*args, **kwargs)
        else:
            super().save(*args, **kwargs)
        self.update_rev_for_option()

    def get_absolute_url(self, params=None):
        url = f"/organizations/{self.organization.slug}/issues/"
        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 str(self.id) == str(value) or str(self.slug) == str(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):
        """ :returns a QuerySet of all Users that belong to this Project """
        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 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,
                             max_length=50)
            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):
        from sentry.incidents.models import AlertRule
        from sentry.models import Rule

        ProjectTeam.objects.filter(project=self, team=team).delete()
        AlertRule.objects.fetch_for_project(self).filter(
            owner_id=team.actor_id).update(owner=None)
        Rule.objects.filter(owner_id=team.actor_id,
                            project=self).update(owner=None)

    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 options.items():
                    self.update_option(key, value)

        except IntegrityError as e:
            logging.exception(
                "Error occurred during copy project settings.",
                extra={
                    "error": str(e),
                    "project_to": self.id,
                    "project_from": project_id,
                },
            )
            return False
        return True

    @staticmethod
    def is_valid_platform(value):
        if not value or value == "other":
            return True
        return integration_doc_exists(value)

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

        # There is no foreign key relationship so we have to manually cascade.
        NotificationSetting.objects.remove_for_project(self)

        return super().delete(**kwargs)
Ejemplo n.º 22
0
class Release(Model):
    """
    A release is generally created when a new version is pushed into a
    production state.
    """
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    projects = models.ManyToManyField('sentry.Project',
                                      related_name='releases',
                                      through=ReleaseProject)
    # DEPRECATED
    project_id = BoundedPositiveIntegerField(null=True)
    version = models.CharField(max_length=DB_VERSION_LENGTH)
    # ref might be the branch name being released
    ref = models.CharField(max_length=DB_VERSION_LENGTH, null=True, blank=True)
    url = models.URLField(null=True, blank=True)
    date_added = models.DateTimeField(default=timezone.now)
    # DEPRECATED - not available in UI or editable from API
    date_started = models.DateTimeField(null=True, blank=True)
    date_released = models.DateTimeField(null=True, blank=True)
    # arbitrary data recorded with the release
    data = JSONField(default={})
    new_groups = BoundedPositiveIntegerField(default=0)
    # generally the release manager, or the person initiating the process
    owner = FlexibleForeignKey('sentry.User',
                               null=True,
                               blank=True,
                               on_delete=models.SET_NULL)

    # materialized stats
    commit_count = BoundedPositiveIntegerField(null=True, default=0)
    last_commit_id = BoundedPositiveIntegerField(null=True)
    authors = ArrayField(null=True)
    total_deploys = BoundedPositiveIntegerField(null=True, default=0)
    last_deploy_id = BoundedPositiveIntegerField(null=True)

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_release'
        unique_together = (('organization', 'version'), )

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

    @staticmethod
    def is_valid_version(value):
        return not (any(c in value
                        for c in BAD_RELEASE_CHARS) or value in ('.', '..')
                    or not value or value.lower() == 'latest')

    @classmethod
    def get_cache_key(cls, organization_id, version):
        return 'release:3:%s:%s' % (organization_id,
                                    md5_text(version).hexdigest())

    @classmethod
    def get_lock_key(cls, organization_id, release_id):
        return u'releasecommits:{}:{}'.format(organization_id, release_id)

    @classmethod
    def get(cls, project, version):
        cache_key = cls.get_cache_key(project.organization_id, version)

        release = cache.get(cache_key)
        if release is None:
            try:
                release = cls.objects.get(
                    organization_id=project.organization_id,
                    projects=project,
                    version=version,
                )
            except cls.DoesNotExist:
                release = -1
            cache.set(cache_key, release, 300)

        if release == -1:
            return

        return release

    @classmethod
    def get_or_create(cls, project, version, date_added=None):
        from sentry.models import Project

        if date_added is None:
            date_added = timezone.now()

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

        release = cache.get(cache_key)
        if release in (None, -1):
            # TODO(dcramer): if the cache result is -1 we could attempt a
            # default create here instead of default get
            project_version = ('%s-%s' %
                               (project.slug, version))[:DB_VERSION_LENGTH]
            releases = list(
                cls.objects.filter(organization_id=project.organization_id,
                                   version__in=[version, project_version],
                                   projects=project))
            if releases:
                try:
                    release = [
                        r for r in releases if r.version == project_version
                    ][0]
                except IndexError:
                    release = releases[0]
            else:
                try:
                    with transaction.atomic():
                        release = cls.objects.create(
                            organization_id=project.organization_id,
                            version=version,
                            date_added=date_added,
                            total_deploys=0,
                        )
                except IntegrityError:
                    release = cls.objects.get(
                        organization_id=project.organization_id,
                        version=version)
                release.add_project(project)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(
                        flags=F('flags').bitor(Project.flags.has_releases))

            # TODO(dcramer): upon creating a new release, check if it should be
            # the new "latest release" for this project
            cache.set(cache_key, release, 3600)

        return release

    @classmethod
    def merge(cls, to_release, from_releases):
        # The following models reference release:
        # ReleaseCommit.release
        # ReleaseEnvironment.release_id
        # ReleaseProject.release
        # GroupRelease.release_id
        # GroupResolution.release
        # Group.first_release
        # ReleaseFile.release

        from sentry.models import (ReleaseCommit, ReleaseEnvironment,
                                   ReleaseFile, ReleaseProject,
                                   ReleaseProjectEnvironment, Group,
                                   GroupRelease, GroupResolution)

        model_list = (ReleaseCommit, ReleaseEnvironment, ReleaseFile,
                      ReleaseProject, ReleaseProjectEnvironment, GroupRelease,
                      GroupResolution)
        for release in from_releases:
            for model in model_list:
                if hasattr(model, 'release'):
                    update_kwargs = {'release': to_release}
                else:
                    update_kwargs = {'release_id': to_release.id}
                try:
                    with transaction.atomic():
                        model.objects.filter(release_id=release.id).update(
                            **update_kwargs)
                except IntegrityError:
                    for item in model.objects.filter(release_id=release.id):
                        try:
                            with transaction.atomic():
                                model.objects.filter(id=item.id).update(
                                    **update_kwargs)
                        except IntegrityError:
                            item.delete()

            Group.objects.filter(first_release=release).update(
                first_release=to_release)

            release.delete()

    @property
    def short_version(self):
        return Release.get_display_version(self.version)

    @staticmethod
    def get_display_version(version):
        match = _dotted_path_prefix_re.match(version)
        if match is not None:
            version = version[match.end():]
        if _sha1_re.match(version):
            return version[:7]
        return version

    def add_dist(self, name, date_added=None):
        from sentry.models import Distribution
        if date_added is None:
            date_added = timezone.now()
        return Distribution.objects.get_or_create(release=self,
                                                  name=name,
                                                  defaults={
                                                      'date_added':
                                                      date_added,
                                                      'organization_id':
                                                      self.organization_id,
                                                  })[0]

    def get_dist(self, name):
        from sentry.models import Distribution
        try:
            return Distribution.objects.get(name=name, release=self)
        except Distribution.DoesNotExist:
            pass

    def add_project(self, project):
        """
        Add a project to this release.

        Returns True if the project was added and did not already exist.
        """
        from sentry.models import Project
        try:
            with transaction.atomic():
                ReleaseProject.objects.create(project=project, release=self)
                if not project.flags.has_releases:
                    project.flags.has_releases = True
                    project.update(flags=F('flags').bitor(
                        Project.flags.has_releases), )
        except IntegrityError:
            return False
        else:
            return True

    def handle_commit_ranges(self, refs):
        """
        Takes commit refs of the form:
        [
            {
                'previousCommit': None,
                'commit': 'previous_commit..commit',
            }
        ]
        Note: Overwrites 'previousCommit' and 'commit'
        """
        for ref in refs:
            if COMMIT_RANGE_DELIMITER in ref['commit']:
                ref['previousCommit'], ref['commit'] = ref['commit'].split(
                    COMMIT_RANGE_DELIMITER)

    def set_refs(self, refs, user, fetch=False):
        from sentry.api.exceptions import InvalidRepository
        from sentry.models import Commit, ReleaseHeadCommit, Repository
        from sentry.tasks.commits import fetch_commits

        # TODO: this does the wrong thing unless you are on the most
        # recent release.  Add a timestamp compare?
        prev_release = type(self).objects.filter(
            organization_id=self.organization_id,
            projects__in=self.projects.all(),
        ).extra(select={
            'sort': 'COALESCE(date_released, date_added)',
        }).exclude(version=self.version).order_by('-sort').first()

        names = {r['repository'] for r in refs}
        repos = list(
            Repository.objects.filter(
                organization_id=self.organization_id,
                name__in=names,
            ))
        repos_by_name = {r.name: r for r in repos}
        invalid_repos = names - set(repos_by_name.keys())
        if invalid_repos:
            raise InvalidRepository('Invalid repository names: %s' %
                                    ','.join(invalid_repos))

        self.handle_commit_ranges(refs)

        for ref in refs:
            repo = repos_by_name[ref['repository']]

            commit = Commit.objects.get_or_create(
                organization_id=self.organization_id,
                repository_id=repo.id,
                key=ref['commit'],
            )[0]
            # update head commit for repo/release if exists
            ReleaseHeadCommit.objects.create_or_update(
                organization_id=self.organization_id,
                repository_id=repo.id,
                release=self,
                values={
                    'commit': commit,
                })
        if fetch:
            fetch_commits.apply_async(
                kwargs={
                    'release_id': self.id,
                    'user_id': user.id,
                    'refs': refs,
                    'prev_release_id': prev_release and prev_release.id,
                })

    def set_commits(self, commit_list):
        """
        Bind a list of commits to this release.

        This will clear any existing commit log and replace it with the given
        commits.
        """

        # Sort commit list in reverse order
        commit_list.sort(key=lambda commit: commit.get('timestamp'),
                         reverse=True)

        # TODO(dcramer): this function could use some cleanup/refactoring as its a bit unwieldly
        from sentry.models import (Commit, CommitAuthor, Group, GroupLink,
                                   GroupResolution, GroupStatus, ReleaseCommit,
                                   ReleaseHeadCommit, Repository, PullRequest)
        from sentry.plugins.providers.repository import RepositoryProvider
        from sentry.tasks.integrations import kick_off_status_syncs
        # todo(meredith): implement for IntegrationRepositoryProvider
        commit_list = [
            c for c in commit_list if
            not RepositoryProvider.should_ignore_commit(c.get('message', ''))
        ]
        lock_key = type(self).get_lock_key(self.organization_id, self.id)
        lock = locks.get(lock_key, duration=10)
        with TimedRetryPolicy(10)(lock.acquire):
            start = time()
            with transaction.atomic():
                # TODO(dcramer): would be good to optimize the logic to avoid these
                # deletes but not overly important
                initial_commit_ids = set(
                    ReleaseCommit.objects.filter(release=self, ).values_list(
                        'commit_id', flat=True))
                ReleaseCommit.objects.filter(release=self, ).delete()

                authors = {}
                repos = {}
                commit_author_by_commit = {}
                head_commit_by_repo = {}
                latest_commit = None
                for idx, data in enumerate(commit_list):
                    repo_name = data.get(
                        'repository') or u'organization-{}'.format(
                            self.organization_id)
                    if repo_name not in repos:
                        repos[
                            repo_name] = repo = Repository.objects.get_or_create(
                                organization_id=self.organization_id,
                                name=repo_name,
                            )[0]
                    else:
                        repo = repos[repo_name]

                    author_email = data.get('author_email')
                    if author_email is None and data.get('author_name'):
                        author_email = (re.sub(r'[^a-zA-Z0-9\-_\.]*', '',
                                               data['author_name']).lower() +
                                        '@localhost')

                    if not author_email:
                        author = None
                    elif author_email not in authors:
                        author_data = {'name': data.get('author_name')}
                        author, created = CommitAuthor.objects.create_or_update(
                            organization_id=self.organization_id,
                            email=author_email,
                            values=author_data)
                        if not created:
                            author = CommitAuthor.objects.get(
                                organization_id=self.organization_id,
                                email=author_email)
                        authors[author_email] = author
                    else:
                        author = authors[author_email]

                    commit_data = {}
                    defaults = {}

                    # Update/set message and author if they are provided.
                    if author is not None:
                        commit_data['author'] = author
                    if 'message' in data:
                        commit_data['message'] = data['message']
                    if 'timestamp' in data:
                        commit_data['date_added'] = data['timestamp']
                    else:
                        defaults['date_added'] = timezone.now()

                    commit, created = Commit.objects.create_or_update(
                        organization_id=self.organization_id,
                        repository_id=repo.id,
                        key=data['id'],
                        defaults=defaults,
                        values=commit_data)
                    if not created:
                        commit = Commit.objects.get(
                            organization_id=self.organization_id,
                            repository_id=repo.id,
                            key=data['id'])

                    if author is None:
                        author = commit.author

                    commit_author_by_commit[commit.id] = author

                    patch_set = data.get('patch_set', [])
                    for patched_file in patch_set:
                        try:
                            with transaction.atomic():
                                CommitFileChange.objects.create(
                                    organization_id=self.organization.id,
                                    commit=commit,
                                    filename=patched_file['path'],
                                    type=patched_file['type'],
                                )
                        except IntegrityError:
                            pass

                    try:
                        with transaction.atomic():
                            ReleaseCommit.objects.create(
                                organization_id=self.organization_id,
                                release=self,
                                commit=commit,
                                order=idx,
                            )
                    except IntegrityError:
                        pass

                    if latest_commit is None:
                        latest_commit = commit

                    head_commit_by_repo.setdefault(repo.id, commit.id)

                self.update(
                    commit_count=len(commit_list),
                    authors=[
                        six.text_type(a_id)
                        for a_id in ReleaseCommit.objects.filter(
                            release=self,
                            commit__author_id__isnull=False,
                        ).values_list('commit__author_id',
                                      flat=True).distinct()
                    ],
                    last_commit_id=latest_commit.id if latest_commit else None,
                )
                metrics.timing('release.set_commits.duration', time() - start)

        # fill any missing ReleaseHeadCommit entries
        for repo_id, commit_id in six.iteritems(head_commit_by_repo):
            try:
                with transaction.atomic():
                    ReleaseHeadCommit.objects.create(
                        organization_id=self.organization_id,
                        release_id=self.id,
                        repository_id=repo_id,
                        commit_id=commit_id,
                    )
            except IntegrityError:
                pass

        release_commits = list(
            ReleaseCommit.objects.filter(
                release=self).select_related('commit').values(
                    'commit_id', 'commit__key'))
        final_commit_ids = set(rc['commit_id'] for rc in release_commits)
        removed_commit_ids = initial_commit_ids - final_commit_ids
        added_commit_ids = final_commit_ids - initial_commit_ids
        if removed_commit_ids or added_commit_ids:
            release_commits_updated.send_robust(
                release=self,
                removed_commit_ids=removed_commit_ids,
                added_commit_ids=added_commit_ids,
                sender=self.__class__,
            )

        commit_resolutions = list(
            GroupLink.objects.filter(
                linked_type=GroupLink.LinkedType.commit,
                linked_id__in=[rc['commit_id'] for rc in release_commits],
            ).values_list('group_id', 'linked_id'))

        commit_group_authors = [
            (
                cr[0],  # group_id
                commit_author_by_commit.get(cr[1]))
            for cr in commit_resolutions
        ]

        pr_ids_by_merge_commit = list(
            PullRequest.objects.filter(
                merge_commit_sha__in=[
                    rc['commit__key'] for rc in release_commits
                ],
                organization_id=self.organization_id,
            ).values_list('id', flat=True))

        pull_request_resolutions = list(
            GroupLink.objects.filter(
                relationship=GroupLink.Relationship.resolves,
                linked_type=GroupLink.LinkedType.pull_request,
                linked_id__in=pr_ids_by_merge_commit,
            ).values_list('group_id', 'linked_id'))

        pr_authors = list(
            PullRequest.objects.filter(id__in=[
                prr[1] for prr in pull_request_resolutions
            ], ).select_related('author'))

        pr_authors_dict = {pra.id: pra.author for pra in pr_authors}

        pull_request_group_authors = [(prr[0], pr_authors_dict.get(prr[1]))
                                      for prr in pull_request_resolutions]

        user_by_author = {None: None}

        commits_and_prs = list(
            itertools.chain(commit_group_authors,
                            pull_request_group_authors), )

        group_project_lookup = dict(
            Group.objects.filter(id__in=[
                group_id for group_id, _ in commits_and_prs
            ], ).values_list('id', 'project_id'))

        for group_id, author in commits_and_prs:
            if author not in user_by_author:
                try:
                    user_by_author[author] = author.find_users()[0]
                except IndexError:
                    user_by_author[author] = None
            actor = user_by_author[author]

            with transaction.atomic():
                GroupResolution.objects.create_or_update(
                    group_id=group_id,
                    values={
                        'release': self,
                        'type': GroupResolution.Type.in_release,
                        'status': GroupResolution.Status.resolved,
                        'actor_id': actor.id if actor else None,
                    },
                )
                group = Group.objects.get(id=group_id, )
                group.update(status=GroupStatus.RESOLVED)
                metrics.incr('group.resolved',
                             instance='in_commit',
                             skip_internal=True)

            issue_resolved.send_robust(
                organization_id=self.organization_id,
                user=actor,
                group=group,
                project=group.project,
                resolution_type='with_commit',
                sender=type(self),
            )

            kick_off_status_syncs.apply_async(
                kwargs={
                    'project_id': group_project_lookup[group_id],
                    'group_id': group_id,
                })
Ejemplo n.º 23
0
class GroupTagValue(Model):
    """
    Stores the total number of messages seen by a group matching
    the given filter.
    """
    __core__ = False

    project = FlexibleForeignKey('sentry.Project',
                                 null=True,
                                 related_name='grouptag')
    group = FlexibleForeignKey('sentry.Group', related_name='grouptag')
    times_seen = BoundedPositiveIntegerField(default=0)
    key = models.CharField(max_length=MAX_TAG_KEY_LENGTH)
    value = models.CharField(max_length=MAX_TAG_VALUE_LENGTH)
    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 = BaseManager()

    class Meta:
        app_label = 'sentry'
        db_table = 'sentry_messagefiltervalue'
        unique_together = (('group', 'key', 'value'), )
        index_together = (('project', 'key', 'value', 'last_seen'), )

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

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

    @classmethod
    def get_value_count(cls, group_id, key):
        if db.is_postgres():
            # This doesnt guarantee percentage is accurate, but it does ensure
            # that the query has a maximum cost
            cursor = connections['default'].cursor()
            cursor.execute(
                """
                SELECT SUM(t)
                FROM (
                    SELECT times_seen as t
                    FROM sentry_messagefiltervalue
                    WHERE group_id = %s
                    AND key = %s
                    ORDER BY last_seen DESC
                    LIMIT 10000
                ) as a
            """, [group_id, key])
            return cursor.fetchone()[0] or 0

        cutoff = timezone.now() - timedelta(days=7)
        return cls.objects.filter(
            group=group_id,
            key=key,
            last_seen__gte=cutoff,
        ).aggregate(t=Sum('times_seen'))['t']

    @classmethod
    def get_top_values(cls, group_id, key, limit=3):
        if db.is_postgres():
            # This doesnt guarantee percentage is accurate, but it does ensure
            # that the query has a maximum cost
            return list(
                cls.objects.raw(
                    """
                SELECT *
                FROM (
                    SELECT *
                    FROM sentry_messagefiltervalue
                    WHERE group_id = %%s
                    AND key = %%s
                    ORDER BY last_seen DESC
                    LIMIT 10000
                ) as a
                ORDER BY times_seen DESC
                LIMIT %d
            """ % limit, [group_id, key]))

        cutoff = timezone.now() - timedelta(days=7)
        return list(
            cls.objects.filter(
                group=group_id,
                key=key,
                last_seen__gte=cutoff,
            ).order_by('-times_seen')[:limit])
Ejemplo n.º 24
0
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """
    project = FlexibleForeignKey('sentry.Project', null=True)
    logger = models.CharField(
        max_length=64, blank=True, default='root', 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')
    checksum = models.CharField(max_length=32, db_index=True)
    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)
    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'
        unique_together = (('project', 'checksum'),)
        verbose_name_plural = _('grouped messages')
        verbose_name = _('grouped message')
        permissions = (
            ("can_view", "Can view"),
        )

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

    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 natural_key(self):
        return (self.project, self.checksum)

    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_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

    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')
        )
Ejemplo n.º 25
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_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)
Ejemplo n.º 26
0
class ProjectCodeOwners(DefaultFieldsModel):
    __include_in_export__ = False
    # no db constraint to prevent locks on the Project table
    project = FlexibleForeignKey("sentry.Project", db_constraint=False)
    # repository_project_path_config ⇒ use this to transform CODEOWNERS paths to stacktrace paths
    repository_project_path_config = FlexibleForeignKey(
        "sentry.RepositoryProjectPathConfig", unique=True, on_delete=models.PROTECT
    )
    # raw ⇒ original CODEOWNERS file.
    raw = models.TextField(null=True)
    # schema ⇒ transformed into IssueOwner syntax
    schema = JSONField(null=True)
    # override date_added from DefaultFieldsModel
    date_added = models.DateTimeField(default=timezone.now)

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

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

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

    @classmethod
    def get_codeowners_cached(self, project_id):
        """
        Cached read access to sentry_projectcodeowners.

        This method implements a negative cache which saves us
        a pile of read queries in post_processing as most projects
        don't have CODEOWNERS.
        """
        cache_key = self.get_cache_key(project_id)
        code_owners = cache.get(cache_key)
        if code_owners is None:
            query = self.objects.filter(project_id=project_id).order_by("-date_added") or False
            code_owners = self.merge_code_owners_list(code_owners_list=query) if query else query
            cache.set(cache_key, code_owners, READ_CACHE_DURATION)

        return code_owners or None

    @classmethod
    def validate_codeowners_associations(self, codeowners, project):
        from sentry.api.endpoints.project_codeowners import validate_association
        from sentry.models import (
            ExternalActor,
            OrganizationMember,
            OrganizationMemberTeam,
            Project,
            UserEmail,
            actor_type_to_string,
        )
        from sentry.ownership.grammar import parse_code_owners
        from sentry.types.integrations import ExternalProviders

        # Get list of team/user names from CODEOWNERS file
        team_names, usernames, emails = parse_code_owners(codeowners)

        # Check if there exists Sentry users with the emails listed in CODEOWNERS
        user_emails = UserEmail.objects.filter(
            email__in=emails,
            user__sentry_orgmember_set__organization=project.organization,
        )

        # Check if the usernames/teamnames have an association
        external_actors = ExternalActor.objects.filter(
            external_name__in=usernames + team_names,
            organization=project.organization,
            provider__in=[ExternalProviders.GITHUB.value, ExternalProviders.GITLAB.value],
        )

        # Convert CODEOWNERS into IssueOwner syntax
        users_dict = {}
        teams_dict = {}
        teams_without_access = []
        users_without_access = []
        for external_actor in external_actors:
            type = actor_type_to_string(external_actor.actor.type)
            if type == "user":
                user = external_actor.actor.resolve()
                organization_members_ids = OrganizationMember.objects.filter(
                    user_id=user.id, organization_id=project.organization_id
                ).values_list("id", flat=True)
                team_ids = OrganizationMemberTeam.objects.filter(
                    organizationmember_id__in=Subquery(organization_members_ids)
                ).values_list("team_id", flat=True)
                projects = Project.objects.get_for_team_ids(Subquery(team_ids))

                if project in projects:
                    users_dict[external_actor.external_name] = user.email
                else:
                    users_without_access.append(f"{user.username}")
            elif type == "team":
                team = external_actor.actor.resolve()
                # make sure the sentry team has access to the project
                # tied to the codeowner
                if project in team.get_projects():
                    teams_dict[external_actor.external_name] = f"#{team.slug}"
                else:
                    teams_without_access.append(f"#{team.slug}")

        emails_dict = {item.email: item.email for item in user_emails}
        associations = {**users_dict, **teams_dict, **emails_dict}

        errors = {
            "missing_user_emails": validate_association(emails, user_emails, "emails"),
            "missing_external_users": validate_association(usernames, external_actors, "usernames"),
            "missing_external_teams": validate_association(
                team_names, external_actors, "team names"
            ),
            "teams_without_access": teams_without_access,
            "users_without_access": users_without_access,
        }
        return associations, errors

    @classmethod
    def merge_code_owners_list(self, code_owners_list):
        """
        Merge list of code_owners into a single code_owners object concating all the rules. We assume schema version is constant.
        """
        merged_code_owners = None
        for code_owners in code_owners_list:
            if code_owners.schema:
                if merged_code_owners is None:
                    merged_code_owners = code_owners
                    continue
                merged_code_owners.schema["rules"] = [
                    *merged_code_owners.schema["rules"],
                    *code_owners.schema["rules"],
                ]

        return merged_code_owners

    def update_schema(self):
        """
        Updating the schema goes through the following steps:
        1. parsing the original codeowner file to get the associations
        2. convert the codeowner file to the ownership syntax
        3. convert the ownership syntax to the schema
        """
        associations, _ = self.validate_codeowners_associations(self.raw, self.project)

        issue_owner_rules = convert_codeowners_syntax(
            codeowners=self.raw,
            associations=associations,
            code_mapping=self.repository_project_path_config,
        )

        # Convert IssueOwner syntax into schema syntax
        try:
            schema = create_schema_from_issue_owners(
                issue_owners=issue_owner_rules, project_id=self.project.id
            )
            # Convert IssueOwner syntax into schema syntax
            if schema:
                self.schema = schema
                self.save()
        except ValidationError:
            return
Ejemplo n.º 27
0
class Group(Model):
    """
    Aggregated message which summarizes a set of Events.
    """

    __core__ = False

    project = FlexibleForeignKey("sentry.Project")
    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(cache_fields=("id",))

    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"), ("project", "id")]
        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):
        url = reverse("sentry-organization-issue", args=[self.organization.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))

    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_from_cache(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):
        if not hasattr(self, "_latest_event"):
            self._latest_event = self.get_latest_event_for_environments()

        return self._latest_event

    def get_latest_event_for_environments(self, environments=()):
        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_for_environments(self, environments=()):
        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"))
class SentryApp(ParanoidModel, HasApiScopes):
    __core__ = True

    application = models.OneToOneField(
        'sentry.ApiApplication',
        null=True,
        on_delete=models.SET_NULL,
        related_name='sentry_app',
    )

    # Much of the OAuth system in place currently depends on a User existing.
    # This "proxy user" represents the SentryApp in those cases.
    proxy_user = models.OneToOneField(
        'sentry.User',
        null=True,
        on_delete=models.SET_NULL,
        related_name='sentry_app'
    )

    # The Organization the Sentry App was created in "owns" it. Members of that
    # Org have differing access, dependent on their role within the Org.
    owner = FlexibleForeignKey('sentry.Organization',
                               related_name='owned_sentry_apps')

    name = models.TextField()
    slug = models.CharField(max_length=SENTRY_APP_SLUG_MAX_LENGTH, unique=True)
    status = BoundedPositiveIntegerField(
        default=SentryAppStatus.UNPUBLISHED,
        choices=SentryAppStatus.as_choices(),
        db_index=True,
    )
    uuid = models.CharField(max_length=64,
                            default=default_uuid)

    redirect_url = models.URLField(null=True)
    webhook_url = models.URLField()
    # does the application subscribe to `event.alert`,
    # meaning can it be used in alert rules as a {service} ?
    is_alertable = models.BooleanField(default=False)

    events = ArrayField(of=models.TextField, null=True)

    overview = models.TextField(null=True)
    schema = EncryptedJsonField(default=dict)

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

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

    @classmethod
    def visible_for_user(cls, request):
        from sentry.auth.superuser import is_active_superuser
        if is_active_superuser(request):
            return cls.objects.all()

        user = request.user
        return cls.objects.filter(
            Q(status=SentryAppStatus.PUBLISHED) | Q(owner__in=user.get_orgs()),
        )

    @property
    def organizations(self):
        if not self.pk:
            return Organization.objects.none()

        return Organization \
            .objects \
            .select_related('sentry_app_installations') \
            .filter(sentry_app_installations__sentry_app_id=self.id)

    @property
    def teams(self):
        from sentry.models import Team

        if not self.pk:
            return Team.objects.none()

        return Team.objects.filter(organization__in=self.organizations)

    @property
    def is_published(self):
        return self.status == SentryAppStatus.PUBLISHED

    def save(self, *args, **kwargs):
        self._set_slug()
        self.date_updated = timezone.now()
        return super(SentryApp, self).save(*args, **kwargs)

    def is_installed_on(self, organization):
        return self.organizations.filter(pk=organization.pk).exists()

    def _set_slug(self):
        """
        Matches ``name``, but in lowercase, dash form.

        >>> self._set_slug('My Cool App')
        >>> self.slug
        my-cool-app
        """
        if not self.slug:
            self.slug = slugify(self.name)

    def build_signature(self, body):
        secret = self.application.client_secret
        return hmac.new(
            key=secret.encode('utf-8'),
            msg=body.encode('utf-8'),
            digestmod=sha256,
        ).hexdigest()
Ejemplo n.º 29
0
class SentryApp(ParanoidModel, HasApiScopes):
    __core__ = True

    application = models.OneToOneField(
        "sentry.ApiApplication", null=True, on_delete=models.SET_NULL, related_name="sentry_app"
    )

    # Much of the OAuth system in place currently depends on a User existing.
    # This "proxy user" represents the SentryApp in those cases.
    proxy_user = models.OneToOneField(
        "sentry.User", null=True, on_delete=models.SET_NULL, related_name="sentry_app"
    )

    # The Organization the Sentry App was created in "owns" it. Members of that
    # Org have differing access, dependent on their role within the Org.
    owner = FlexibleForeignKey("sentry.Organization", related_name="owned_sentry_apps")

    name = models.TextField()
    slug = models.CharField(max_length=SENTRY_APP_SLUG_MAX_LENGTH, unique=True)
    author = models.TextField(null=True)
    status = BoundedPositiveIntegerField(
        default=SentryAppStatus.UNPUBLISHED, choices=SentryAppStatus.as_choices(), db_index=True
    )
    uuid = models.CharField(max_length=64, default=default_uuid)

    redirect_url = models.URLField(null=True)
    webhook_url = models.URLField(null=True)
    # does the application subscribe to `event.alert`,
    # meaning can it be used in alert rules as a {service} ?
    is_alertable = models.BooleanField(default=False)

    # does the application need to wait for verification
    # on behalf of the external service to know if its installations
    # are successfully installed ?
    verify_install = models.BooleanField(default=True)

    events = ArrayField(of=models.TextField, null=True)

    overview = models.TextField(null=True)
    schema = EncryptedJsonField(default=dict)

    date_added = models.DateTimeField(default=timezone.now)
    date_updated = models.DateTimeField(default=timezone.now)
    date_published = models.DateTimeField(null=True, blank=True)

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

    @classmethod
    def visible_for_user(cls, request):
        from sentry.auth.superuser import is_active_superuser

        if is_active_superuser(request):
            return cls.objects.all()

        return cls.objects.filter(status=SentryAppStatus.PUBLISHED)

    # this method checks if a user from a sentry app has permission to a specific project
    # for now, only checks if app is installed on the org of the project
    @classmethod
    def check_project_permission_for_sentry_app_user(cls, user, project):
        assert user.is_sentry_app
        # if the user exists, so should the sentry_app
        sentry_app = cls.objects.get(proxy_user=user)
        return sentry_app.is_installed_on(project.organization)

    @property
    def is_published(self):
        return self.status == SentryAppStatus.PUBLISHED

    @property
    def is_unpublished(self):
        return self.status == SentryAppStatus.UNPUBLISHED

    @property
    def is_internal(self):
        return self.status == SentryAppStatus.INTERNAL

    @property
    def slug_for_metrics(self):
        if self.is_internal:
            return "internal"
        if self.is_unpublished:
            return "unpublished"
        return self.slug

    def save(self, *args, **kwargs):
        self.date_updated = timezone.now()
        return super(SentryApp, self).save(*args, **kwargs)

    def is_installed_on(self, organization):
        return SentryAppInstallation.objects.filter(organization=organization).exists()

    def build_signature(self, body):
        secret = self.application.client_secret
        return hmac.new(
            key=secret.encode("utf-8"), msg=body.encode("utf-8"), digestmod=sha256
        ).hexdigest()

    def show_auth_info(self, access):
        encoded_scopes = set({u"%s" % scope for scope in list(access.scopes)})
        return set(self.scope_list).issubset(encoded_scopes)
Ejemplo n.º 30
0
class ReleaseFile(Model):
    r"""
    A ReleaseFile is an association between a Release and a File.

    The ident of the file should be sha1(name) or
    sha1(name '\x00\x00' dist.name) and must be unique per release.
    """
    __core__ = False

    organization = FlexibleForeignKey('sentry.Organization')
    # DEPRECATED
    project_id = BoundedBigIntegerField(null=True)
    release = FlexibleForeignKey('sentry.Release')
    file = FlexibleForeignKey('sentry.File')
    ident = models.CharField(max_length=40)
    name = models.TextField()
    dist = FlexibleForeignKey('sentry.Distribution', null=True)

    __repr__ = sane_repr('release', 'ident')

    class Meta:
        unique_together = (('release', 'ident'), )
        index_together = (('release', 'name'), )
        app_label = 'sentry'
        db_table = 'sentry_releasefile'

    def save(self, *args, **kwargs):
        if not self.ident and self.name:
            dist = self.dist_id and self.dist.name or None
            self.ident = type(self).get_ident(self.name, dist)
        return super(ReleaseFile, self).save(*args, **kwargs)

    def update(self, *args, **kwargs):
        # If our name is changing, we must also change the ident
        if 'name' in kwargs and 'ident' not in kwargs:
            dist = kwargs.get('dist') or self.dist
            kwargs['ident'] = self.ident = type(self).get_ident(
                kwargs['name'], dist and dist.name or dist
            )
        return super(ReleaseFile, self).update(*args, **kwargs)

    @classmethod
    def get_ident(cls, name, dist=None):
        if dist is not None:
            return sha1_text(name + '\x00\x00' + dist).hexdigest()
        return sha1_text(name).hexdigest()

    @classmethod
    def normalize(cls, url):
        """Transforms a full absolute url into 2 or 4 generalized options

        * the original url as input
        * (optional) original url without querystring
        * the full url, but stripped of scheme and netloc
        * (optional) full url without scheme and netloc or querystring
        """
        # Always ignore the fragment
        scheme, netloc, path, query, _ = urlsplit(url)

        uri_without_fragment = (scheme, netloc, path, query, None)
        uri_relative = (None, None, path, query, None)
        uri_without_query = (scheme, netloc, path, None, None)
        uri_relative_without_query = (None, None, path, None, None)
        urls = [urlunsplit(uri_without_fragment)]
        if query:
            urls.append(urlunsplit(uri_without_query))
        urls.append('~' + urlunsplit(uri_relative))
        if query:
            urls.append('~' + urlunsplit(uri_relative_without_query))
        return urls