예제 #1
0
class OrganizationMemberSerializer(serializers.Serializer):
    email = AllowedEmailField(max_length=75, required=True)
    role = serializers.ChoiceField(choices=roles.get_choices(), required=True)
    teams = ListField(required=False, allow_null=False)
    sendInvite = serializers.BooleanField(required=False,
                                          default=True,
                                          write_only=True)
예제 #2
0
class OrganizationSettingsForm(forms.ModelForm):
    name = forms.CharField(
        help_text=_('The name of your organization. i.e. My Company'))
    slug = forms.SlugField(
        label=_('Short name'),
        help_text=_('A unique ID used to identify this organization.'),
    )
    allow_joinleave = forms.BooleanField(
        label=_('Open Membership'),
        help_text=_(
            'Allow organization members to freely join or leave any team.'),
        required=False,
    )
    default_role = forms.ChoiceField(
        label=_('Default Role'),
        choices=roles.get_choices(),
        help_text=_('The default role new members will receive.'),
    )
    enhanced_privacy = forms.BooleanField(
        label=_('Enhanced Privacy'),
        help_text=
        _('Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.'
          ),
        required=False,
    )

    class Meta:
        fields = ('name', 'slug', 'default_role')
        model = Organization
예제 #3
0
class OwnerOrganizationSerializer(OrganizationSerializer):
    defaultRole = serializers.ChoiceField(choices=roles.get_choices())

    def save(self, *args, **kwargs):
        org = self.context['organization']
        if 'defaultRole' in self.init_data:
            org.default_role = self.init_data['defaultRole']
        return super(OwnerOrganizationSerializer, self).save(*args, **kwargs)
class AuthProviderSettingsForm(forms.Form):
    require_link = forms.BooleanField(
        label=_('Require SSO'),
        help_text=_('Require members use a valid linked SSO account to access this organization'),
        required=False,
    )
    default_role = forms.ChoiceField(
        label=_('Default Role'),
        choices=roles.get_choices(),
        help_text=_('The default role new members will receive when logging in for the first time.'),
    )
예제 #5
0
class OwnerOrganizationSerializer(OrganizationSerializer):
    defaultRole = serializers.ChoiceField(choices=roles.get_choices())
    cancelDeletion = serializers.BooleanField(required=False)

    def save(self, *args, **kwargs):
        org = self.context['organization']
        cancel_deletion = 'cancelDeletion' in self.init_data and org.status in DELETION_STATUSES
        if 'defaultRole' in self.init_data:
            org.default_role = self.init_data['defaultRole']
        if cancel_deletion:
            org.status = OrganizationStatus.VISIBLE
        return super(OwnerOrganizationSerializer, self).save(*args, **kwargs)
예제 #6
0
class OwnerOrganizationSerializer(OrganizationSerializer):
    defaultRole = serializers.ChoiceField(choices=roles.get_choices())
    cancelDeletion = serializers.BooleanField(required=False)

    def save(self, *args, **kwargs):
        org = self.context["organization"]
        update_tracked_data(org)
        cancel_deletion = "cancelDeletion" in self.initial_data and org.status in DELETION_STATUSES
        if "defaultRole" in self.initial_data:
            org.default_role = self.initial_data["defaultRole"]
        if cancel_deletion:
            org.status = OrganizationStatus.VISIBLE
        return super().save(*args, **kwargs)
class OrganizationMemberSerializer(serializers.Serializer):
    email = AllowedEmailField(max_length=75, required=True)
    role = serializers.ChoiceField(choices=roles.get_choices(), required=True)
    teams = ListField(required=False, allow_null=False, default=[])
    sendInvite = serializers.BooleanField(required=False,
                                          default=True,
                                          write_only=True)

    def validate_email(self, email):
        queryset = OrganizationMember.objects.filter(
            Q(email=email)
            | Q(user__email__iexact=email, user__is_active=True),
            organization=self.context["organization"],
        )

        if queryset.filter(invite_status=InviteStatus.APPROVED.value).exists():
            raise serializers.ValidationError(
                "The user %s is already a member" % email)

        if not self.context.get("allow_existing_invite_request"):
            if queryset.filter(
                    Q(invite_status=InviteStatus.REQUESTED_TO_BE_INVITED.value)
                    | Q(invite_status=InviteStatus.REQUESTED_TO_JOIN.value)
            ).exists():
                raise serializers.ValidationError(
                    "There is an existing invite request for %s" % email)
        return email

    def validate_teams(self, teams):
        valid_teams = list(
            Team.objects.filter(organization=self.context["organization"],
                                status=TeamStatus.VISIBLE,
                                slug__in=teams))

        if len(valid_teams) != len(teams):
            raise serializers.ValidationError("Invalid teams")

        return valid_teams

    def validate_role(self, role):
        if role not in {r.id for r in self.context["allowed_roles"]}:
            raise serializers.ValidationError(
                "You do not have permission to invite that role.")

        return role
예제 #8
0
class OrganizationSettingsForm(forms.ModelForm):
    name = forms.CharField(
        help_text=_('The name of your organization. i.e. My Company'))
    slug = forms.SlugField(
        help_text=_('A unique ID used to identify this organization.'))
    allow_joinleave = forms.BooleanField(
        label=_('Open Membership'),
        help_text=_(
            'Allow organization members to freely join or leave any team.'),
        required=False,
    )
    default_role = forms.ChoiceField(
        label=_('Default Role'),
        choices=roles.get_choices(),
        help_text=_('The default role new members will receive.'),
    )

    class Meta:
        fields = ('name', 'slug', 'default_role')
        model = Organization
예제 #9
0
    class AuthProviderSettingsForm(forms.Form):
        require_link = forms.BooleanField(
            label=_("Require SSO"),
            help_text=
            _("Require members use a valid linked SSO account to access this organization"
              ),
            required=False,
        )

        enable_scim = (forms.BooleanField(
            label=_("Enable SCIM"),
            help_text=_(
                "Enable SCIM to manage Memberships and Teams via your Provider"
            ),
            required=False,
        ) if provider.can_use_scim(organization, request.user) else None)

        default_role = forms.ChoiceField(
            label=_("Default Role"),
            choices=roles.get_choices(),
            help_text=
            _("The default role new members will receive when logging in for the first time."
              ),
        )
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.
    """
    __core__ = True

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

    user = FlexibleForeignKey(
        settings.AUTH_USER_MODEL, null=True, blank=True, related_name="sentry_orgmember_set"
    )
    email = models.EmailField(null=True, blank=True)
    role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )
    flags = BitField(
        flags=(('sso:linked', 'sso:linked'), ('sso:invalid', 'sso:invalid'), ), default=0
    )
    token = models.CharField(max_length=64, null=True, blank=True, unique=True)
    date_added = models.DateTimeField(default=timezone.now)
    has_global_access = models.BooleanField(default=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)

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

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

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

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

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

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

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

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

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

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

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

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

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

        email = self.get_email()

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

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

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

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

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

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

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

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

    def get_audit_log_data(self):
        from sentry.models import Team
        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)
                ).values_list('id', 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
예제 #11
0
class OrganizationMember(Model):
    """
    Identifies relationships between organizations and users.

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

    __core__ = True

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        email = self.get_email()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    @classmethod
    def delete_expired(cls, threshold):
        """
        Delete un-accepted member invitations that expired
        ``threshold`` days ago.
        """
        cls.objects.filter(token_expires_at__lt=threshold, user_id__exact=None).exclude(
            email__exact=None
        ).delete()
예제 #12
0
class Organization(Model):
    """
    An organization represents a group of individuals which maintain ownership of projects.
    """
    __core__ = True

    name = models.CharField(max_length=64)
    slug = models.SlugField(unique=True)
    status = BoundedPositiveIntegerField(
        choices=OrganizationStatus.as_choices(),
        # south will generate a default value of `'<OrganizationStatus.ACTIVE: 0>'`
        # if `.value` is omitted
        default=OrganizationStatus.ACTIVE.value)
    date_added = models.DateTimeField(default=timezone.now)
    members = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                     through='sentry.OrganizationMember',
                                     related_name='org_memberships')
    default_role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )

    flags = BitField(flags=(
        ('allow_joinleave',
         'Allow members to join and leave teams without requiring approval.'),
        ('enhanced_privacy',
         'Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.'
         ),
        ('disable_shared_issues',
         'Disable sharing of limited details on issues to anonymous users.'),
        ('early_adopter',
         'Enable early adopter status, gaining access to features prior to public release.'
         ),
        ('require_2fa',
         'Require and enforce two-factor authentication for all members.'),
        (
            'disable_new_visibility_features',
            'Temporarily opt out of new visibility features and ui',
        ),
    ),
                     default=1)

    objects = OrganizationManager(cache_fields=(
        'pk',
        'slug',
    ))

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

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

    @classmethod
    def get_default(cls):
        """
        Return the organization used in single organization mode.
        """
        return cls.objects.filter(status=OrganizationStatus.ACTIVE, )[0]

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

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

    def delete(self):
        if self.is_default:
            raise Exception('You cannot delete the the default organization.')
        return super(Organization, self).delete()

    @cached_property
    def is_default(self):
        if not settings.SENTRY_SINGLE_ORGANIZATION:
            return False

        return self == type(self).get_default()

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

        return queryset.exists()

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

    def get_owners(self):
        from sentry.models import User
        return User.objects.filter(
            sentry_orgmember_set__role=roles.get_top_dog().id,
            sentry_orgmember_set__organization=self,
            is_active=True,
        )

    def get_default_owner(self):
        if not hasattr(self, '_default_owner'):
            self._default_owner = self.get_owners()[0]
        return self._default_owner

    def has_single_owner(self):
        from sentry.models import OrganizationMember
        count = OrganizationMember.objects.filter(
            organization=self,
            role=roles.get_top_dog().id,
            user__isnull=False,
            user__is_active=True,
        )[:2].count()
        return count == 1

    def merge_to(from_org, to_org):
        from sentry.models import (
            ApiKey,
            AuditLogEntry,
            AuthProvider,
            Commit,
            OrganizationAvatar,
            OrganizationIntegration,
            OrganizationMember,
            OrganizationMemberTeam,
            Project,
            Release,
            ReleaseCommit,
            ReleaseEnvironment,
            ReleaseFile,
            ReleaseHeadCommit,
            Repository,
            Team,
            Environment,
        )

        for from_member in OrganizationMember.objects.filter(
                organization=from_org, user__isnull=False):
            logger = logging.getLogger('sentry.merge')
            try:
                to_member = OrganizationMember.objects.get(
                    organization=to_org,
                    user=from_member.user,
                )
            except OrganizationMember.DoesNotExist:
                from_member.update(organization=to_org)
                to_member = from_member
            else:
                qs = OrganizationMemberTeam.objects.filter(
                    organizationmember=from_member,
                    is_active=True,
                ).select_related()
                for omt in qs:
                    OrganizationMemberTeam.objects.create_or_update(
                        organizationmember=to_member,
                        team=omt.team,
                        defaults={
                            'is_active': True,
                        },
                    )
            logger.info('user.migrate',
                        extra={
                            'instance_id': from_member.id,
                            'new_member_id': to_member.id,
                            'from_organization_id': from_org.id,
                            'to_organization_id': to_org.id,
                        })

        for from_team in Team.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    from_team.update(organization=to_org)
            except IntegrityError:
                slugify_instance(from_team,
                                 from_team.name,
                                 organization=to_org)
                from_team.update(
                    organization=to_org,
                    slug=from_team.slug,
                )
            logger.info('team.migrate',
                        extra={
                            'instance_id': from_team.id,
                            'new_slug': from_team.slug,
                            'from_organization_id': from_org.id,
                            'to_organization_id': to_org.id,
                        })

        for from_project in Project.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    from_project.update(organization=to_org)
            except IntegrityError:
                slugify_instance(from_project,
                                 from_project.name,
                                 organization=to_org,
                                 reserved=RESERVED_PROJECT_SLUGS)
                from_project.update(
                    organization=to_org,
                    slug=from_project.slug,
                )
            logger.info('project.migrate',
                        extra={
                            'instance_id': from_project.id,
                            'new_slug': from_project.slug,
                            'from_organization_id': from_org.id,
                            'to_organization_id': to_org.id,
                        })

        # TODO(jess): update this when adding unique constraint
        # on version, organization for releases
        for from_release in Release.objects.filter(organization=from_org):
            try:
                to_release = Release.objects.get(version=from_release.version,
                                                 organization=to_org)
            except Release.DoesNotExist:
                Release.objects.filter(id=from_release.id).update(
                    organization=to_org)
            else:
                Release.merge(to_release, [from_release])
            logger.info('release.migrate',
                        extra={
                            'instance_id': from_release.id,
                            'from_organization_id': from_org.id,
                            'to_organization_id': to_org.id,
                        })

        def do_update(queryset, params):
            model_name = queryset.model.__name__.lower()
            try:
                with transaction.atomic():
                    queryset.update(**params)
            except IntegrityError:
                for instance in queryset:
                    try:
                        with transaction.atomic():
                            instance.update(**params)
                    except IntegrityError:
                        logger.info('{}.migrate-skipped'.format(model_name),
                                    extra={
                                        'from_organization_id': from_org.id,
                                        'to_organization_id': to_org.id,
                                    })
                    else:
                        logger.info('{}.migrate'.format(model_name),
                                    extra={
                                        'instance_id': instance.id,
                                        'from_organization_id': from_org.id,
                                        'to_organization_id': to_org.id,
                                    })
            else:
                logger.info('{}.migrate'.format(model_name),
                            extra={
                                'from_organization_id': from_org.id,
                                'to_organization_id': to_org.id,
                            })

        INST_MODEL_LIST = (
            AuthProvider,
            ApiKey,
            AuditLogEntry,
            OrganizationAvatar,
            OrganizationIntegration,
            ReleaseEnvironment,
            ReleaseFile,
        )

        ATTR_MODEL_LIST = (
            Commit,
            ReleaseCommit,
            ReleaseHeadCommit,
            Repository,
            Environment,
        )

        for model in INST_MODEL_LIST:
            queryset = model.objects.filter(organization=from_org, )
            do_update(queryset, {'organization': to_org})

        for model in ATTR_MODEL_LIST:
            queryset = model.objects.filter(organization_id=from_org.id, )
            do_update(queryset, {'organization_id': to_org.id})

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

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

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

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

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

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

    def send_delete_confirmation(self, audit_log_entry, countdown):
        from sentry import options
        from sentry.utils.email import MessageBuilder

        owners = self.get_owners()

        context = {
            'organization':
            self,
            'audit_log_entry':
            audit_log_entry,
            'eta':
            timezone.now() + timedelta(seconds=countdown),
            'url':
            absolute_uri(
                reverse(
                    'sentry-restore-organization',
                    args=[self.slug],
                )),
        }

        MessageBuilder(
            subject='%sOrganization Queued for Deletion' %
            (options.get('mail.subject-prefix'), ),
            template='sentry/emails/org_delete_confirm.txt',
            html_template='sentry/emails/org_delete_confirm.html',
            type='org.confirm_delete',
            context=context,
        ).send_async([o.email for o in owners])

    def flag_has_changed(self, flag_name):
        "Returns ``True`` if ``flag`` has changed since initialization."
        return getattr(self.old_value('flags'), flag_name, None) != getattr(
            self.flags, flag_name)

    def handle_2fa_required(self, request):
        from sentry.models import ApiKey
        from sentry.tasks.auth import remove_2fa_non_compliant_members

        actor_id = request.user.id if request.user and request.user.is_authenticated(
        ) else None
        api_key_id = request.auth.id if hasattr(
            request, 'auth') and isinstance(request.auth, ApiKey) else None
        ip_address = request.META['REMOTE_ADDR']

        remove_2fa_non_compliant_members.delay(self.id,
                                               actor_id=actor_id,
                                               actor_key_id=api_key_id,
                                               ip_address=ip_address)

    def get_url_viewname(self):
        from sentry import features
        if features.has('organizations:sentry10', self):
            return 'sentry-organization-issue-list'
        else:
            return 'sentry-organization-home'

    def get_url(self):
        return reverse(self.get_url_viewname(), args=[self.slug])
예제 #13
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.
    """
    __core__ = True

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

    user = FlexibleForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
                             related_name="sentry_orgmember_set")
    email = models.EmailField(null=True, blank=True)
    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',
            type='organization.invite',
            context=context,
        )

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

    def send_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',
            type='organization.auth_link',
            context=context,
        )
        msg.send_async([self.get_email()])

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

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

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

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

    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
예제 #14
0
class OrganizationMemberSerializer(serializers.Serializer):
    reinvite = serializers.BooleanField()
    regenerate = serializers.BooleanField()
    role = serializers.ChoiceField(choices=roles.get_choices(), required=True)
    teams = ListField(required=False, allow_null=False)
예제 #15
0
class Organization(Model):
    """
    An organization represents a group of individuals which maintain ownership of projects.
    """
    __core__ = True

    name = models.CharField(max_length=64)
    slug = models.SlugField(unique=True)
    status = BoundedPositiveIntegerField(
        choices=OrganizationStatus.as_choices(),
        default=OrganizationStatus.ACTIVE)
    date_added = models.DateTimeField(default=timezone.now)
    members = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                     through='sentry.OrganizationMember',
                                     related_name='org_memberships')
    default_role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )

    flags = BitField(flags=(
        ('allow_joinleave',
         'Allow members to join and leave teams without requiring approval.'),
        ('enhanced_privacy',
         'Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.'
         ),
        ('disable_shared_issues',
         'Disable sharing of limited details on issues to anonymous users.'),
        ('early_adopter',
         'Enable early adopter status, gaining access to features prior to public release.'
         ),
        ('require_2fa',
         'Require and enforce two-factor authentication for all members.'),
    ),
                     default=1)

    objects = OrganizationManager(cache_fields=(
        'pk',
        'slug',
    ))

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

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

    @classmethod
    def get_default(cls):
        """
        Return the organization used in single organization mode.
        """
        return cls.objects.filter(status=OrganizationStatus.ACTIVE, )[0]

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

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

    def delete(self):
        if self.is_default:
            raise Exception('You cannot delete the the default organization.')
        return super(Organization, self).delete()

    @cached_property
    def is_default(self):
        if not settings.SENTRY_SINGLE_ORGANIZATION:
            return False

        return self == type(self).get_default()

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

        return queryset.exists()

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

    def get_owners(self):
        from sentry.models import User
        return User.objects.filter(
            sentry_orgmember_set__role=roles.get_top_dog().id,
            sentry_orgmember_set__organization=self,
            is_active=True,
        )

    def get_default_owner(self):
        if not hasattr(self, '_default_owner'):
            self._default_owner = self.get_owners()[0]
        return self._default_owner

    def has_single_owner(self):
        from sentry.models import OrganizationMember
        count = OrganizationMember.objects.filter(
            organization=self,
            role=roles.get_top_dog().id,
            user__isnull=False,
            user__is_active=True,
        )[:2].count()
        return count == 1

    def merge_to(from_org, to_org):
        from sentry.models import (
            ApiKey,
            AuditLogEntry,
            Commit,
            OrganizationMember,
            OrganizationMemberTeam,
            Project,
            Release,
            ReleaseCommit,
            ReleaseEnvironment,
            ReleaseFile,
            ReleaseHeadCommit,
            Repository,
            Team,
            Environment,
        )

        for from_member in OrganizationMember.objects.filter(
                organization=from_org, user__isnull=False):
            try:
                to_member = OrganizationMember.objects.get(
                    organization=to_org,
                    user=from_member.user,
                )
            except OrganizationMember.DoesNotExist:
                from_member.update(organization=to_org)
                to_member = from_member
            else:
                qs = OrganizationMemberTeam.objects.filter(
                    organizationmember=from_member,
                    is_active=True,
                ).select_related()
                for omt in qs:
                    OrganizationMemberTeam.objects.create_or_update(
                        organizationmember=to_member,
                        team=omt.team,
                        defaults={
                            'is_active': True,
                        },
                    )

        for team in Team.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    team.update(organization=to_org)
            except IntegrityError:
                slugify_instance(team, team.name, organization=to_org)
                team.update(
                    organization=to_org,
                    slug=team.slug,
                )

        for project in Project.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    project.update(organization=to_org)
            except IntegrityError:
                slugify_instance(project, project.name, organization=to_org)
                project.update(
                    organization=to_org,
                    slug=project.slug,
                )

        # TODO(jess): update this when adding unique constraint
        # on version, organization for releases
        for release in Release.objects.filter(organization=from_org):
            try:
                to_release = Release.objects.get(version=release.version,
                                                 organization=to_org)
            except Release.DoesNotExist:
                Release.objects.filter(id=release.id).update(
                    organization=to_org)
            else:
                Release.merge(to_release, [release])

        for model in (ApiKey, AuditLogEntry, ReleaseFile):
            model.objects.filter(
                organization=from_org, ).update(organization=to_org)

        for model in (Commit, ReleaseCommit, ReleaseEnvironment,
                      ReleaseHeadCommit, Repository, Environment):
            try:
                with transaction.atomic():
                    model.objects.filter(organization_id=from_org.id, ).update(
                        organization_id=to_org.id)
            except IntegrityError:
                pass

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

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

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

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

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

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

    def send_delete_confirmation(self, audit_log_entry, countdown):
        from sentry import options
        from sentry.utils.email import MessageBuilder

        owners = self.get_owners()

        context = {
            'organization':
            self,
            'audit_log_entry':
            audit_log_entry,
            'eta':
            timezone.now() + timedelta(seconds=countdown),
            'url':
            absolute_uri(
                reverse(
                    'sentry-restore-organization',
                    args=[self.slug],
                )),
        }

        MessageBuilder(
            subject='%sOrganization Queued for Deletion' %
            (options.get('mail.subject-prefix'), ),
            template='sentry/emails/org_delete_confirm.txt',
            html_template='sentry/emails/org_delete_confirm.html',
            type='org.confirm_delete',
            context=context,
        ).send_async([o.email for o in owners])

    def flag_has_changed(self, flag_name):
        "Returns ``True`` if ``flag`` has changed since initialization."
        return getattr(self.old_value('flags'), flag_name, None) != getattr(
            self.flags, flag_name)

    def send_setup_2fa_emails(self):
        from sentry import options
        from sentry.utils.email import MessageBuilder
        from sentry.models import User

        for user in User.objects.filter(
                is_active=True,
                sentry_orgmember_set__organization=self,
        ):
            if not Authenticator.objects.user_has_2fa(user):
                context = {
                    'user': user,
                    'url':
                    absolute_uri(reverse('sentry-account-settings-2fa')),
                    'organization': self
                }
                message = MessageBuilder(
                    subject='%s %s Mandatory: Enable Two-Factor Authentication'
                    % (options.get('mail.subject-prefix'), self.name),
                    template='sentry/emails/setup_2fa.txt',
                    html_template='sentry/emails/setup_2fa.html',
                    type='user.setup_2fa',
                    context=context,
                )
                message.send_async([user.email])
예제 #16
0
class OrganizationSettingsForm(forms.ModelForm):
    name = forms.CharField(
        help_text=_('The name of your organization. i.e. My Company'))
    slug = forms.SlugField(
        label=_('Short name'),
        help_text=_('A unique ID used to identify this organization.'),
    )
    allow_joinleave = forms.BooleanField(
        label=_('Open Membership'),
        help_text=_(
            'Allow organization members to freely join or leave any team.'),
        required=False,
    )
    default_role = forms.ChoiceField(
        label=_('Default Role'),
        choices=roles.get_choices(),
        help_text=_('The default role new members will receive.'),
    )
    enhanced_privacy = forms.BooleanField(
        label=_('Enhanced Privacy'),
        help_text=
        _('Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.'
          ),
        required=False,
    )
    allow_shared_issues = forms.BooleanField(
        label=_('Allow Shared Issues'),
        help_text=_(
            'Enable sharing of limited details on issues to anonymous users.'),
        required=False,
    )
    require_scrub_data = forms.BooleanField(
        label=_('Require Data Scrubber'),
        help_text=_(
            'Require server-side data scrubbing be enabled for all projects.'),
        required=False)
    require_scrub_defaults = forms.BooleanField(
        label=_('Require Using Default Scrubbers'),
        help_text=
        _('Require the default scrubbers be applied to prevent things like passwords and credit cards from being stored for all projects.'
          ),
        required=False)
    sensitive_fields = forms.CharField(
        label=_('Global additional sensitive fields'),
        help_text=
        _('Additional field names to match against when scrubbing data for all projects. '
          'Separate multiple entries with a newline.<br /><strong>Note: These fields will be used in addition to project specific fields.</strong>'
          ),
        widget=forms.Textarea(
            attrs={
                'placeholder': mark_safe(_('e.g. email')),
                'class': 'span8',
                'rows': '3',
            }),
        required=False,
    )
    require_scrub_ip_address = forms.BooleanField(
        label=_('Require not storing IP Addresses'),
        help_text=
        _('Require preventing IP addresses from being stored for new events on all projects.'
          ),
        required=False)

    class Meta:
        fields = ('name', 'slug', 'default_role')
        model = Organization

    def clean_sensitive_fields(self):
        value = self.cleaned_data.get('sensitive_fields')
        if not value:
            return

        return filter(bool, (v.lower().strip() for v in value.split('\n')))
예제 #17
0
class Organization(Model):
    """
    An organization represents a group of individuals which maintain ownership of projects.
    """
    name = models.CharField(max_length=64)
    slug = models.SlugField(unique=True)
    status = BoundedPositiveIntegerField(choices=(
        (OrganizationStatus.VISIBLE, _('Visible')),
        (OrganizationStatus.PENDING_DELETION, _('Pending Deletion')),
        (OrganizationStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
    ),
                                         default=OrganizationStatus.VISIBLE)
    date_added = models.DateTimeField(default=timezone.now)
    members = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                     through='sentry.OrganizationMember',
                                     related_name='org_memberships')
    default_role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )

    flags = BitField(flags=((
        'allow_joinleave',
        'Allow members to join and leave teams without requiring approval.'),
                            ),
                     default=1)

    objects = OrganizationManager(cache_fields=(
        'pk',
        'slug',
    ))

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

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

    @classmethod
    def get_default(cls):
        """
        Return the organization used in single organization mode.
        """
        return cls.objects.filter(status=OrganizationStatus.VISIBLE, )[0]

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

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

    def delete(self):
        if self.is_default:
            raise Exception('You cannot delete the the default organization.')
        return super(Organization, self).delete()

    @cached_property
    def is_default(self):
        if not settings.SENTRY_SINGLE_ORGANIZATION:
            return False

        return self == type(self).get_default()

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

        return queryset.exists()

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

    def get_default_owner(self):
        if not hasattr(self, '_default_owner'):
            from sentry.models import User

            self._default_owner = User.objects.filter(
                sentry_orgmember_set__role=roles.get_top_dog().id,
                sentry_orgmember_set__organization=self,
            )[0]
        return self._default_owner

    def has_single_owner(self):
        from sentry.models import OrganizationMember
        count = OrganizationMember.objects.filter(
            organization=self,
            role='owner',
            user__isnull=False,
        ).count()
        return count == 1

    def merge_to(from_org, to_org):
        from sentry.models import (ApiKey, AuditLogEntry, OrganizationMember,
                                   OrganizationMemberTeam, Project, Team)

        for from_member in OrganizationMember.objects.filter(
                organization=from_org):
            try:
                to_member = OrganizationMember.objects.get(
                    organization=to_org,
                    user=from_member.user,
                )
            except OrganizationMember.DoesNotExist:
                from_member.update(organization=to_org)
                to_member = from_member
            else:
                qs = OrganizationMemberTeam.objects.filter(
                    organizationmember=from_member,
                    is_active=True,
                ).select_related()
                for omt in qs:
                    OrganizationMemberTeam.objects.create_or_update(
                        organizationmember=to_member,
                        team=omt.team,
                        defaults={
                            'is_active': True,
                        },
                    )
        for model in (Team, Project, ApiKey, AuditLogEntry):
            model.objects.filter(
                organization=from_org, ).update(organization=to_org)
class OrganizationMemberSerializer(serializers.Serializer):
    email = AllowedEmailField(max_length=75, required=True)
    role = serializers.ChoiceField(choices=roles.get_choices(), required=True)
    teams = ListField(required=False, allow_null=False)
예제 #19
0
class Organization(Model):
    """
    An organization represents a group of individuals which maintain ownership of projects.
    """
    name = models.CharField(max_length=64)
    slug = models.SlugField(unique=True)
    status = BoundedPositiveIntegerField(choices=(
        (OrganizationStatus.VISIBLE, _('Visible')),
        (OrganizationStatus.PENDING_DELETION, _('Pending Deletion')),
        (OrganizationStatus.DELETION_IN_PROGRESS, _('Deletion in Progress')),
    ),
                                         default=OrganizationStatus.VISIBLE)
    date_added = models.DateTimeField(default=timezone.now)
    members = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                     through='sentry.OrganizationMember',
                                     related_name='org_memberships')
    default_role = models.CharField(
        choices=roles.get_choices(),
        max_length=32,
        default=roles.get_default().id,
    )

    flags = BitField(flags=(
        ('allow_joinleave',
         'Allow members to join and leave teams without requiring approval.'),
        ('enhanced_privacy',
         'Enable enhanced privacy controls to limit personally identifiable information (PII) as well as source code in things like notifications.'
         ),
        ('disable_shared_issues',
         'Disable sharing of limited details on issues to anonymous users.'),
        ('early_adopter',
         'Enable early adopter status, gaining access to features prior to public release.'
         ),
    ),
                     default=1)

    objects = OrganizationManager(cache_fields=(
        'pk',
        'slug',
    ))

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

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

    @classmethod
    def get_default(cls):
        """
        Return the organization used in single organization mode.
        """
        return cls.objects.filter(status=OrganizationStatus.VISIBLE, )[0]

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

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

    def delete(self):
        if self.is_default:
            raise Exception('You cannot delete the the default organization.')
        return super(Organization, self).delete()

    @cached_property
    def is_default(self):
        if not settings.SENTRY_SINGLE_ORGANIZATION:
            return False

        return self == type(self).get_default()

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

        return queryset.exists()

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

    def get_default_owner(self):
        if not hasattr(self, '_default_owner'):
            from sentry.models import User

            self._default_owner = User.objects.filter(
                sentry_orgmember_set__role=roles.get_top_dog().id,
                sentry_orgmember_set__organization=self,
            )[0]
        return self._default_owner

    def has_single_owner(self):
        from sentry.models import OrganizationMember
        count = OrganizationMember.objects.filter(
            organization=self,
            role='owner',
            user__isnull=False,
        ).count()
        return count == 1

    def merge_to(from_org, to_org):
        from sentry.models import (ApiKey, AuditLogEntry, OrganizationMember,
                                   OrganizationMemberTeam, Project, Team)

        for from_member in OrganizationMember.objects.filter(
                organization=from_org):
            try:
                to_member = OrganizationMember.objects.get(
                    organization=to_org,
                    user=from_member.user,
                )
            except OrganizationMember.DoesNotExist:
                from_member.update(organization=to_org)
                to_member = from_member
            else:
                qs = OrganizationMemberTeam.objects.filter(
                    organizationmember=from_member,
                    is_active=True,
                ).select_related()
                for omt in qs:
                    OrganizationMemberTeam.objects.create_or_update(
                        organizationmember=to_member,
                        team=omt.team,
                        defaults={
                            'is_active': True,
                        },
                    )

        for team in Team.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    team.update(organization=to_org)
            except IntegrityError:
                slugify_instance(team, team.name, organization=to_org)
                team.update(
                    organization=to_org,
                    slug=team.slug,
                )

        for project in Project.objects.filter(organization=from_org):
            try:
                with transaction.atomic():
                    project.update(organization=to_org)
            except IntegrityError:
                slugify_instance(project, project.name, organization=to_org)
                project.update(
                    organization=to_org,
                    slug=project.slug,
                )

        for model in (ApiKey, AuditLogEntry):
            model.objects.filter(
                organization=from_org, ).update(organization=to_org)

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

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

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

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

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

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