class Authenticator(BaseModel): id = BoundedAutoField(primary_key=True) user = FlexibleForeignKey('sentry.User', db_index=True) created_at = models.DateTimeField(_('created at'), default=timezone.now) last_used_at = models.DateTimeField(_('last used at'), null=True) type = BoundedPositiveIntegerField(choices=AUTHENTICATOR_CHOICES) config = UnicodePickledObjectField() objects = AuthenticatorManager() class Meta: app_label = 'sentry' db_table = 'auth_authenticator' verbose_name = _('authenticator') verbose_name_plural = _('authenticators') @cached_property def interface(self): return AUTHENTICATOR_INTERFACES_BY_TYPE[self.type](self) def mark_used(self, save=True): self.last_used_at = timezone.now() if save: self.save() def __repr__(self): return '<Authenticator user=%r interface=%r>' % ( self.user.email, self.interface.interface_id, )
class OrganizationMemberTeam(BaseModel): """ Identifies relationships between organization members and the teams they are on. """ __core__ = True id = BoundedAutoField(primary_key=True) team = FlexibleForeignKey("sentry.Team") organizationmember = FlexibleForeignKey("sentry.OrganizationMember") # an inactive membership simply removes the team from the default list # but still allows them to re-join without request is_active = models.BooleanField(default=True) class Meta: app_label = "sentry" db_table = "sentry_organizationmember_teams" unique_together = (("team", "organizationmember"), ) __repr__ = sane_repr("team_id", "organizationmember_id") def get_audit_log_data(self): return { "team_slug": self.team.slug, "member_id": self.organizationmember_id, "email": self.organizationmember.get_email(), "is_active": self.is_active, }
class OrganizationMemberTeam(BaseModel): """ Identifies relationships between organization members and the teams they are on. """ __core__ = True id = BoundedAutoField(primary_key=True) team = FlexibleForeignKey('sentry.Team') organizationmember = FlexibleForeignKey('sentry.OrganizationMember') # an inactive membership simply removes the team from the default list # but still allows them to re-join without request is_active = models.BooleanField(default=True) class Meta: app_label = 'sentry' db_table = 'sentry_organizationmember_teams' unique_together = (('team', 'organizationmember'), ) __repr__ = sane_repr('team_id', 'organizationmember_id') def get_audit_log_data(self): return { 'team_slug': self.team.slug, 'member_id': self.organizationmember_id, 'email': self.organizationmember.get_email(), 'is_active': self.is_active, }
class Authenticator(BaseModel): __core__ = True id = BoundedAutoField(primary_key=True) user = FlexibleForeignKey("sentry.User", db_index=True) created_at = models.DateTimeField(_("created at"), default=timezone.now) last_used_at = models.DateTimeField(_("last used at"), null=True) type = BoundedPositiveIntegerField(choices=AUTHENTICATOR_CHOICES) config = EncryptedPickledObjectField() objects = AuthenticatorManager() class AlreadyEnrolled(Exception): pass class Meta: app_label = "sentry" db_table = "auth_authenticator" verbose_name = _("authenticator") verbose_name_plural = _("authenticators") unique_together = (("user", "type"), ) @cached_property def interface(self): return AUTHENTICATOR_INTERFACES_BY_TYPE[self.type](self) def mark_used(self, save=True): self.last_used_at = timezone.now() if save: self.save() def reset_fields(self, save=True): self.created_at = timezone.now() self.last_used_at = None if save: self.save() def __repr__(self): return "<Authenticator user=%r interface=%r>" % ( self.user.email, self.interface.interface_id, )
class User(BaseModel, AbstractBaseUser): __include_in_export__ = True id = BoundedAutoField(primary_key=True) username = models.CharField(_("username"), max_length=128, unique=True) # this column is called first_name for legacy reasons, but it is the entire # display name name = models.CharField(_("name"), max_length=200, blank=True, db_column="first_name") email = models.EmailField(_("email address"), blank=True, max_length=75) is_staff = models.BooleanField( _("staff status"), default=False, help_text=_( "Designates whether the user can log into this admin site."), ) is_active = models.BooleanField( _("active"), default=True, help_text=_("Designates whether this user should be treated as " "active. Unselect this instead of deleting accounts."), ) is_superuser = models.BooleanField( _("superuser status"), default=False, help_text= _("Designates that this user has all permissions without explicitly assigning them." ), ) is_managed = models.BooleanField( _("managed"), default=False, help_text=_("Designates whether this user should be treated as " "managed. Select this to disallow the user from " "modifying their account (username, password, etc)."), ) is_sentry_app = models.NullBooleanField( _("is sentry app"), null=True, default=None, help_text=_( "Designates whether this user is the entity used for Permissions" "on behalf of a Sentry App. Cannot login or use Sentry like a" "normal User would."), ) is_password_expired = models.BooleanField( _("password expired"), default=False, help_text=_("If set to true then the user needs to change the " "password on next sign in."), ) last_password_change = models.DateTimeField( _("date of last password change"), null=True, help_text=_("The date the password was changed last."), ) flags = BitField( flags=(("newsletter_consent_prompt", "Do we need to ask this user for newsletter consent?"), ), default=0, null=True, ) session_nonce = models.CharField(max_length=12, null=True) actor = FlexibleForeignKey("sentry.Actor", db_index=True, unique=True, null=True, on_delete=models.PROTECT) date_joined = models.DateTimeField(_("date joined"), default=timezone.now) last_active = models.DateTimeField(_("last active"), default=timezone.now, null=True) objects = UserManager(cache_fields=["pk"]) USERNAME_FIELD = "username" REQUIRED_FIELDS = ["email"] class Meta: app_label = "sentry" db_table = "auth_user" verbose_name = _("user") verbose_name_plural = _("users") __repr__ = sane_repr("id") def delete(self): if self.username == "sentry": raise Exception( 'You cannot delete the "sentry" user as it is required by Sentry.' ) avatar = self.avatar.first() if avatar: avatar.delete() return super().delete() def save(self, *args, **kwargs): if not self.username: self.username = self.email return super().save(*args, **kwargs) def has_perm(self, perm_name): warnings.warn("User.has_perm is deprecated", DeprecationWarning) return self.is_superuser def has_module_perms(self, app_label): warnings.warn("User.has_module_perms is deprecated", DeprecationWarning) return self.is_superuser def get_unverified_emails(self): return self.emails.filter(is_verified=False) def get_verified_emails(self): return self.emails.filter(is_verified=True) def has_unverified_emails(self): return self.get_unverified_emails().exists() def has_usable_password(self): if self.password == "" or self.password is None: # This is the behavior we've been relying on from Django 1.6 - 2.0. # In 2.1, a "" or None password is considered usable. # Removing this override requires identifying all the places # to put set_unusable_password and a migration. return False return super().has_usable_password() def get_label(self): return self.email or self.username or self.id def get_display_name(self): return self.name or self.email or self.username def get_full_name(self): return self.name def get_short_name(self): return self.username def get_salutation_name(self): name = self.name or self.username.split("@", 1)[0].split(".", 1)[0] first_name = name.split(" ", 1)[0] return first_name.capitalize() def get_avatar_type(self): avatar = self.avatar.first() if avatar: return avatar.get_avatar_type_display() return "letter_avatar" def send_confirm_email_singular(self, email, is_new_user=False): from sentry import options from sentry.utils.email import MessageBuilder if not email.hash_is_valid(): email.set_hash() email.save() context = { "user": self, "url": absolute_uri( reverse("sentry-account-confirm-email", args=[self.id, email.validation_hash])), "confirm_email": email.email, "is_new_user": is_new_user, } msg = MessageBuilder( subject="{}Confirm Email".format( options.get("mail.subject-prefix")), template="sentry/emails/confirm_email.txt", html_template="sentry/emails/confirm_email.html", type="user.confirm_email", context=context, ) msg.send_async([email.email]) def send_confirm_emails(self, is_new_user=False): email_list = self.get_unverified_emails() for email in email_list: self.send_confirm_email_singular(email, is_new_user) def merge_to(from_user, to_user): # TODO: we could discover relations automatically and make this useful from sentry import roles from sentry.models import ( Activity, AuditLogEntry, Authenticator, AuthIdentity, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, OrganizationMember, OrganizationMemberTeam, UserAvatar, UserEmail, UserOption, ) audit_logger.info("user.merge", extra={ "from_user_id": from_user.id, "to_user_id": to_user.id }) for obj in OrganizationMember.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) # this will error if both users are members of obj.org except IntegrityError: pass # identify the highest priority membership # only applies if both users are members of obj.org # if roles are different, grants combined user the higher of the two to_member = OrganizationMember.objects.get( organization=obj.organization_id, user=to_user) if roles.get(obj.role).priority > roles.get( to_member.role).priority: to_member.update(role=obj.role) for team in obj.teams.all(): try: with transaction.atomic(): OrganizationMemberTeam.objects.create( organizationmember=to_member, team=team) # this will error if both users are on the same team in obj.org, # in which case, no need to update anything except IntegrityError: pass model_list = ( Authenticator, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, UserAvatar, UserEmail, UserOption, ) for model in model_list: for obj in model.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass Activity.objects.filter(user=from_user).update(user=to_user) # users can be either the subject or the object of actions which get logged AuditLogEntry.objects.filter(actor=from_user).update(actor=to_user) AuditLogEntry.objects.filter(target_user=from_user).update( target_user=to_user) # remove any SSO identities that exist on from_user that might conflict # with to_user's existing identities (only applies if both users have # SSO identities in the same org), then pass the rest on to to_user AuthIdentity.objects.filter( user=from_user, auth_provider__organization__in=AuthIdentity.objects.filter( user=to_user).values("auth_provider__organization"), ).delete() AuthIdentity.objects.filter(user=from_user).update(user=to_user) def set_password(self, raw_password): super().set_password(raw_password) self.last_password_change = timezone.now() self.is_password_expired = False def refresh_session_nonce(self, request=None): from django.utils.crypto import get_random_string self.session_nonce = get_random_string(12) if request is not None: request.session["_nonce"] = self.session_nonce def get_orgs(self): from sentry.models import Organization return Organization.objects.get_for_user_ids({self.id}) def get_projects(self): from sentry.models import Project return Project.objects.get_for_user_ids({self.id}) def get_orgs_require_2fa(self): from sentry.models import Organization, OrganizationStatus return Organization.objects.filter( flags=models.F("flags").bitor(Organization.flags.require_2fa), status=OrganizationStatus.VISIBLE, member_set__user=self, ) def clear_lost_passwords(self): LostPasswordHash.objects.filter(user=self).delete()
class User(BaseModel, AbstractBaseUser): __core__ = True id = BoundedAutoField(primary_key=True) username = models.CharField(_('username'), max_length=128, unique=True) # this column is called first_name for legacy reasons, but it is the entire # display name name = models.CharField(_('name'), max_length=200, blank=True, db_column='first_name') email = models.EmailField(_('email address'), blank=True, max_length=75) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.')) is_active = models.BooleanField( _('active'), default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.')) is_superuser = models.BooleanField( _('superuser status'), default=False, help_text=_('Designates that this user has all permissions without ' 'explicitly assigning them.')) is_managed = models.BooleanField( _('managed'), default=False, help_text=_('Designates whether this user should be treated as ' 'managed. Select this to disallow the user from ' 'modifying their account (username, password, etc).')) is_sentry_app = models.NullBooleanField( _('is sentry app'), null=True, default=None, help_text=_( 'Designates whether this user is the entity used for Permissions' 'on behalf of a Sentry App. Cannot login or use Sentry like a' 'normal User would.')) is_password_expired = models.BooleanField( _('password expired'), default=False, help_text=_('If set to true then the user needs to change the ' 'password on next sign in.')) last_password_change = models.DateTimeField( _('date of last password change'), null=True, help_text=_('The date the password was changed last.')) flags = BitField( flags=(('newsletter_consent_prompt', 'Do we need to ask this user for newsletter consent?'), ), default=0, null=True, ) session_nonce = models.CharField(max_length=12, null=True) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) last_active = models.DateTimeField(_('last active'), default=timezone.now, null=True) objects = UserManager(cache_fields=['pk']) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: app_label = 'sentry' db_table = 'auth_user' verbose_name = _('user') verbose_name_plural = _('users') def delete(self): if self.username == 'sentry': raise Exception( 'You cannot delete the "sentry" user as it is required by Sentry.' ) avatar = self.avatar.first() if avatar: avatar.delete() return super(User, self).delete() def save(self, *args, **kwargs): if not self.username: self.username = self.email return super(User, self).save(*args, **kwargs) def has_perm(self, perm_name): warnings.warn('User.has_perm is deprecated', DeprecationWarning) return self.is_superuser def has_module_perms(self, app_label): warnings.warn('User.has_module_perms is deprecated', DeprecationWarning) return self.is_superuser def get_unverified_emails(self): return self.emails.filter(is_verified=False) def get_verified_emails(self): return self.emails.filter(is_verified=True) def has_unverified_emails(self): return self.get_unverified_emails().exists() def get_label(self): return self.email or self.username or self.id def get_display_name(self): return self.name or self.email or self.username def get_full_name(self): return self.name def get_short_name(self): return self.username def get_salutation_name(self): name = self.name or self.username.split('@', 1)[0].split('.', 1)[0] first_name = name.split(' ', 1)[0] return first_name.capitalize() def get_avatar_type(self): avatar = self.avatar.first() if avatar: return avatar.get_avatar_type_display() return 'letter_avatar' def send_confirm_email_singular(self, email, is_new_user=False): from sentry import options from sentry.utils.email import MessageBuilder if not email.hash_is_valid(): email.set_hash() email.save() context = { 'user': self, 'url': absolute_uri( reverse('sentry-account-confirm-email', args=[self.id, email.validation_hash])), 'confirm_email': email.email, 'is_new_user': is_new_user, } msg = MessageBuilder( subject='%sConfirm Email' % (options.get('mail.subject-prefix'), ), template='sentry/emails/confirm_email.txt', html_template='sentry/emails/confirm_email.html', type='user.confirm_email', context=context, ) msg.send_async([email.email]) def send_confirm_emails(self, is_new_user=False): email_list = self.get_unverified_emails() for email in email_list: self.send_confirm_email_singular(email, is_new_user) def merge_to(from_user, to_user): # TODO: we could discover relations automatically and make this useful from sentry import roles from sentry.models import ( Activity, AuditLogEntry, AuthIdentity, Authenticator, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, OrganizationMember, OrganizationMemberTeam, UserAvatar, UserEmail, UserOption, ) audit_logger.info('user.merge', extra={ 'from_user_id': from_user.id, 'to_user_id': to_user.id, }) for obj in OrganizationMember.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass # identify the highest priority membership to_member = OrganizationMember.objects.get( organization=obj.organization_id, user=to_user, ) if roles.get(obj.role).priority > roles.get( to_member.role).priority: to_member.update(role=obj.role) for team in obj.teams.all(): try: with transaction.atomic(): OrganizationMemberTeam.objects.create( organizationmember=to_member, team=team, ) except IntegrityError: pass model_list = ( Authenticator, GroupAssignee, GroupBookmark, GroupSeen, GroupShare, GroupSubscription, Identity, UserAvatar, UserEmail, UserOption, ) for model in model_list: for obj in model.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass Activity.objects.filter(user=from_user, ).update(user=to_user) AuditLogEntry.objects.filter(actor=from_user, ).update(actor=to_user) AuditLogEntry.objects.filter( target_user=from_user, ).update(target_user=to_user) # remove any duplicate identities that exist on the current user that # might conflict w/ the new users existing SSO AuthIdentity.objects.filter( user=from_user, auth_provider__organization__in=AuthIdentity.objects.filter( user=to_user, ).values( 'auth_provider__organization')).delete() AuthIdentity.objects.filter(user=from_user, ).update(user=to_user) def set_password(self, raw_password): super(User, self).set_password(raw_password) self.last_password_change = timezone.now() self.is_password_expired = False def refresh_session_nonce(self, request=None): from django.utils.crypto import get_random_string self.session_nonce = get_random_string(12) if request is not None: request.session['_nonce'] = self.session_nonce def get_orgs(self): from sentry.models import (Organization, OrganizationMember, OrganizationStatus) return Organization.objects.filter( status=OrganizationStatus.VISIBLE, id__in=OrganizationMember.objects.filter( user=self, ).values('organization'), ) def get_orgs_require_2fa(self): from sentry.models import (Organization, OrganizationStatus) return Organization.objects.filter( flags=models.F('flags').bitor(Organization.flags.require_2fa), status=OrganizationStatus.VISIBLE, member_set__user=self, ) def clear_lost_passwords(self): LostPasswordHash.objects.filter(user=self).delete()
class User(BaseModel, AbstractBaseUser): id = BoundedAutoField(primary_key=True) username = models.CharField(_('username'), max_length=128, unique=True) first_name = models.CharField(_('first name'), max_length=30, blank=True) last_name = models.CharField(_('last name'), max_length=30, blank=True) email = models.EmailField(_('email address'), blank=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.')) is_active = models.BooleanField( _('active'), default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.')) is_superuser = models.BooleanField( _('superuser status'), default=False, help_text=_('Designates that this user has all permissions without ' 'explicitly assigning them.')) is_managed = models.BooleanField( _('managed'), default=False, help_text=_('Designates whether this user should be treated as ' 'managed. Select this to disallow the user from ' 'modifying their account (username, password, etc).')) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) objects = UserManager(cache_fields=['pk']) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: app_label = 'sentry' db_table = 'auth_user' verbose_name = _('user') verbose_name_plural = _('users') def delete(self): if self.username == 'sentry': raise Exception( 'You cannot delete the "sentry" user as it is required by Sentry.' ) return super(User, self).delete() def save(self, *args, **kwargs): if not self.username: self.username = self.email return super(User, self).save(*args, **kwargs) def has_perm(self, perm_name): warnings.warn('User.has_perm is deprecated', DeprecationWarning) return self.is_superuser def has_module_perms(self, app_label): # the admin requires this method return self.is_superuser def get_full_name(self): return self.first_name def get_short_name(self): return self.username def merge_to(from_user, to_user): # TODO: we could discover relations automatically and make this useful from sentry.models import (AuditLogEntry, Activity, AuthIdentity, GroupBookmark, Organization, OrganizationMember, UserOption) for obj in Organization.objects.filter(owner=from_user): obj.update(owner=to_user) for obj in OrganizationMember.objects.filter(user=from_user): with transaction.atomic(): try: obj.update(user=to_user) except IntegrityError: pass for obj in GroupBookmark.objects.filter(user=from_user): with transaction.atomic(): try: obj.update(user=to_user) except IntegrityError: pass for obj in UserOption.objects.filter(user=from_user): with transaction.atomic(): try: obj.update(user=to_user) except IntegrityError: pass Activity.objects.filter(user=from_user, ).update(user=to_user) AuditLogEntry.objects.filter(actor=from_user, ).update(actor=to_user) AuditLogEntry.objects.filter( target_user=from_user, ).update(target_user=to_user) AuthIdentity.objects.filter(user=from_user, ).update(user=to_user) def get_display_name(self): return self.first_name or self.email or self.username def is_active_superuser(self): # TODO(dcramer): add VPN support via INTERNAL_IPS + ipaddr ranges return self.is_superuser
class User(BaseModel, AbstractBaseUser): id = BoundedAutoField(primary_key=True) username = models.CharField(_('username'), max_length=128, unique=True) # this column is called first_name for legacy reasons, but it is the entire # display name name = models.CharField(_('name'), max_length=200, blank=True, db_column='first_name') email = models.EmailField(_('email address'), blank=True) is_staff = models.BooleanField( _('staff status'), default=False, help_text=_('Designates whether the user can log into this admin ' 'site.')) is_active = models.BooleanField( _('active'), default=True, help_text=_('Designates whether this user should be treated as ' 'active. Unselect this instead of deleting accounts.')) is_superuser = models.BooleanField( _('superuser status'), default=False, help_text=_('Designates that this user has all permissions without ' 'explicitly assigning them.')) is_managed = models.BooleanField( _('managed'), default=False, help_text=_('Designates whether this user should be treated as ' 'managed. Select this to disallow the user from ' 'modifying their account (username, password, etc).')) date_joined = models.DateTimeField(_('date joined'), default=timezone.now) objects = UserManager(cache_fields=['pk']) USERNAME_FIELD = 'username' REQUIRED_FIELDS = ['email'] class Meta: app_label = 'sentry' db_table = 'auth_user' verbose_name = _('user') verbose_name_plural = _('users') def delete(self): if self.username == 'sentry': raise Exception( 'You cannot delete the "sentry" user as it is required by Sentry.' ) avatar = self.avatar.first() if avatar: avatar.delete() return super(User, self).delete() def save(self, *args, **kwargs): if not self.username: self.username = self.email return super(User, self).save(*args, **kwargs) def has_perm(self, perm_name): warnings.warn('User.has_perm is deprecated', DeprecationWarning) return self.is_superuser def has_module_perms(self, app_label): warnings.warn('User.has_module_perms is deprecated', DeprecationWarning) return self.is_superuser def get_label(self): return self.email or self.username or self.id def get_display_name(self): return self.name or self.email or self.username def get_full_name(self): return self.name def get_short_name(self): return self.username def get_avatar_type(self): avatar = self.avatar.first() if avatar: return avatar.get_avatar_type_display() return 'letter_avatar' def merge_to(from_user, to_user): # TODO: we could discover relations automatically and make this useful from sentry import roles from sentry.models import (AuditLogEntry, Activity, AuthIdentity, GroupAssignee, GroupBookmark, GroupSeen, OrganizationMember, OrganizationMemberTeam, UserAvatar, UserOption) for obj in OrganizationMember.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass # identify the highest priority membership to_member = OrganizationMember.objects.get( organization=obj.organization_id, user=to_user, ) if roles.get(obj.role).priority > roles.get( to_member.role).priority: to_member.update(role=obj.role) for team in obj.teams.all(): try: with transaction.atomic(): OrganizationMemberTeam.objects.create( organizationmember=to_member, team=team, ) except IntegrityError: pass model_list = (GroupAssignee, GroupBookmark, GroupSeen, UserAvatar, UserOption) for model in model_list: for obj in model.objects.filter(user=from_user): try: with transaction.atomic(): obj.update(user=to_user) except IntegrityError: pass Activity.objects.filter(user=from_user, ).update(user=to_user) AuditLogEntry.objects.filter(actor=from_user, ).update(actor=to_user) AuditLogEntry.objects.filter( target_user=from_user, ).update(target_user=to_user) # remove any duplicate identities that exist on the current user that # might conflict w/ the new users existing SSO AuthIdentity.objects.filter( user=from_user, auth_provider__organization__in=AuthIdentity.objects.filter( user=to_user, ).values( 'auth_provider__organization')).delete() AuthIdentity.objects.filter(user=from_user, ).update(user=to_user)