def test_require_2fa__cannot_delete_last_auth(self, email_log): self._require_2fa_for_organization() # enroll in one auth method interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse( 'sentry-api-0-user-authenticator-details', kwargs={ 'user_id': self.user.id, 'auth_id': auth.id, } ) resp = self.client.delete(url, format='json') assert resp.status_code == 403, (resp.status_code, resp.content) self.assertIn('requires 2FA', resp.content) assert Authenticator.objects.filter( id=auth.id, ).exists() assert email_log.info.call_count == 0
def setUp(self): self.owner = self.create_user() self.org = self.create_organization(owner=self.owner) self.member = self.create_user() self.member_om = self.create_member( organization=self.org, user=self.member, role="member", teams=[] ) self.login_as(self.member) totp = TotpInterface() totp.enroll(self.member) self.interface_id = totp.authenticator.id assert Authenticator.objects.filter(user=self.member).exists()
def test_send_setup_2fa_emails_no_non_compliant_members(self): owner = self.create_user('*****@*****.**') TotpInterface().enroll(owner) org = self.create_organization(owner=owner) for num in range(0, 10): user = self.create_user('*****@*****.**' % num) self.create_member(organization=org, user=user) TotpInterface().enroll(user) with self.options({'system.url-prefix': 'http://example.com'}), self.tasks(): org.send_setup_2fa_emails() assert len(mail.outbox) == 0
def setUp(self): self.owner = self.create_user() self.org = self.create_organization(owner=self.owner) self.member = self.create_user() self.member_om = self.create_member( organization=self.org, user=self.member, role='member', teams=[], ) self.login_as(self.member) totp = TotpInterface() totp.enroll(self.member) self.interface_id = totp.authenticator.id assert Authenticator.objects.filter(user=self.member).exists()
def setUp(self): self.user = self.create_user('*****@*****.**') self.org = self.create_organization(owner=self.user, name='saml2-org') # enable require 2FA and enroll user TotpInterface().enroll(self.user) self.org.update( flags=models.F('flags').bitor(Organization.flags.require_2fa)) assert self.org.flags.require_2fa.is_set self.auth_provider = AuthProvider.objects.create( provider=self.provider_name, config=dummy_provider_config, organization=self.org, ) # The system.url-prefix, which is used to generate absolute URLs, must # have a TLD for the SAML2 library to consider the URL generated for # the ACS endpoint valid. self.url_prefix = settings.SENTRY_OPTIONS.get('system.url-prefix') settings.SENTRY_OPTIONS.update({ 'system.url-prefix': 'http://testserver.com', }) super(AuthSAML2Test, self).setUp()
def test_require_2fa__delete_with_multiple_auth__ok(self, email_log): self._require_2fa_for_organization() new_options = settings.SENTRY_OPTIONS.copy() new_options["sms.twilio-account"] = "twilio-account" with self.settings(SENTRY_OPTIONS=new_options): # enroll in two auth methods interface = SmsInterface() interface.phone_number = "5551231234" interface.enroll(self.user) interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse( "sentry-api-0-user-authenticator-details", kwargs={ "user_id": self.user.id, "auth_id": auth.id }, ) resp = self.client.delete(url, format="json") assert resp.status_code == 204, (resp.status_code, resp.content) assert not Authenticator.objects.filter(id=auth.id).exists() self._assert_security_email_sent("mfa-removed", email_log)
def test_member_disable_all_2fa_blocked(self): TotpInterface().enroll(self.no_2fa_user) self.login_as(self.no_2fa_user) self.assert_can_access_org_home() Authenticator.objects.get(user=self.no_2fa_user).delete() self.assert_redirected_to_2fa()
def setUp(self): # 2FA enforced org self.org_2fa = self.create_organization(owner=self.create_user()) self.enable_org_2fa(self.org_2fa) self.no_2fa_user = self.create_user() self.create_member(organization=self.org_2fa, user=self.no_2fa_user, role="member") # 2FA not enforced org self.owner = self.create_user() self.organization = self.create_organization(owner=self.owner) self.manager = self.create_user() self.create_member(organization=self.organization, user=self.manager, role="manager") self.org_user = self.create_user() self.create_member(organization=self.organization, user=self.org_user, role="member") # 2FA enrolled user self.has_2fa = self.create_user() TotpInterface().enroll(self.has_2fa) self.create_member(organization=self.organization, user=self.has_2fa, role="manager") assert Authenticator.objects.user_has_2fa(self.has_2fa)
def test_owner_can_set_2fa_single_member(self): org = self.create_organization(owner=self.owner) TotpInterface().enroll(self.owner) with self.options({"system.url-prefix": "http://example.com"}), self.tasks(): self.assert_can_enable_org_2fa(org, self.owner) assert len(mail.outbox) == 0
def test_org_requires_2fa_with_enrolled_user(self): self.org_require_2fa() user = self.create_user() self.create_member(user=user, organization=self.org, role="member") TotpInterface().enroll(user) assert self.has_object_perm("GET", self.org, user=user)
def test_can_enforce_2fa_with_non_saml_sso_enabled(self): org = self.create_organization(owner=self.owner) TotpInterface().enroll(self.owner) self.auth_provider = AuthProvider.objects.create( provider='github', organization=org, ) self.assert_can_enable_org_2fa(self.organization, self.owner)
def test_member_disable_all_2fa_blocked(self): TotpInterface().enroll(self.no_2fa_user) self.login_as(self.no_2fa_user) self.assert_can_access_org_details(self.path) Authenticator.objects.get(user=self.no_2fa_user).delete() self.assert_cannot_access_org_details(self.path)
def test_2fa_no_signin(self): TotpInterface().enroll(self.user) resp = self.client.post(self.path, { 'password': '******', 'confirm_password': '******' }) assert resp.status_code == 302 assert self.client.session.get('_auth_user_id') is None
def test_new_member_must_enable_2fa(self): new_user = self.create_user() self.create_member(organization=self.org_2fa, user=new_user, role="member") self.login_as(new_user) self.assert_cannot_access_org_details(self.path) TotpInterface().enroll(new_user) self.assert_can_access_org_details(self.path)
def test_cannot_reset_member_2fa__org_requires_2fa(self): self.login_as(self.owner) TotpInterface().enroll(self.owner) self.org.update(flags=F("flags").bitor(Organization.flags.require_2fa)) assert self.org.flags.require_2fa.is_set is True self.assert_cannot_remove_authenticators()
def test_manager_can_set_2fa(self): org = self.create_organization(owner=self.owner) self.create_member(organization=org, user=self.manager, role="manager") self.assert_cannot_enable_org_2fa(org, self.manager, 400) TotpInterface().enroll(self.manager) with self.options({"system.url-prefix": "http://example.com"}), self.tasks(): self.assert_can_enable_org_2fa(org, self.manager) self.assert_2fa_email_equal(mail.outbox, [self.owner.email])
def test_new_member_must_enable_2fa(self): new_user = self.create_user() self.create_member(organization=self.org_2fa, user=new_user, role="member") self.login_as(new_user) self.assert_redirected_to_2fa() TotpInterface().enroll(new_user) self.assert_can_access_org_home()
def test_remove_2fa_password(self): user = self.create_user('*****@*****.**') TotpInterface().enroll(user) path = reverse('sentry-account-settings-2fa-totp') self.login_as(user) resp = self.client.post(path, data={'remove': ''}) assert resp.status_code == 200 self.assertTemplateUsed('sentry/account/twofactor/remove.html') self.assertContains(resp, 'Do you want to remove the method?') self.assertContains(resp, 'Sentry account password')
def test_security_renders_with_2fa(self): user = self.create_user('*****@*****.**') self.login_as(user) TotpInterface().enroll(user) path = reverse('sentry-account-security') resp = self.client.get(path) self.assertTemplateUsed('sentry/account/security.html') assert 'has_2fa' in resp.context assert resp.context['has_2fa'] is True self.assertContains(resp, 'Manage')
def _create_user_and_member(self, has_2fa=False, has_user_email=True, has_member_email=False): user = self._create_user(has_email=has_user_email) if has_2fa: TotpInterface().enroll(user) if has_member_email: email = uuid4().hex member = self.create_member(organization=self.org, user=user, email=email) else: member = self.create_member(organization=self.org, user=user) return user, member
def add_2fa_users_to_org(self, organization, num_of_users, num_with_2fa): non_compliant_members = [] for num in range(0, num_of_users): user = self.create_user('*****@*****.**' % num) self.create_member(organization=organization, user=user) if num % num_with_2fa: TotpInterface().enroll(user) else: non_compliant_members.append(user.email) return non_compliant_members
def test_bulk_users_have_2fa(self): user1 = self.create_user("*****@*****.**") user2 = self.create_user("*****@*****.**") TotpInterface().enroll(user1) assert Authenticator.objects.bulk_users_have_2fa([user1.id, user2.id, 9999]) == { user1.id: True, user2.id: False, 9999: False, }
def test_send_setup_2fa_emails(self): owner = self.create_user('*****@*****.**') TotpInterface().enroll(owner) org = self.create_organization(owner=owner) non_compliant_members = [] for num in range(0, 10): user = self.create_user('*****@*****.**' % num) self.create_member(organization=org, user=user) if num % 2: TotpInterface().enroll(user) else: non_compliant_members.append(user.email) with self.options({'system.url-prefix': 'http://example.com'}), self.tasks(): org.send_setup_2fa_emails() assert len(mail.outbox) == len(non_compliant_members) assert sorted([email.to[0] for email in mail.outbox ]) == sorted(non_compliant_members)
def test_require_2fa__cannot_delete_last_auth(self, email_log): self._require_2fa_for_organization() # enroll in one auth method interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse('sentry-api-0-user-authenticator-details', kwargs={ 'user_id': self.user.id, 'auth_id': auth.id, }) resp = self.client.delete(url, format='json') assert resp.status_code == 403, (resp.status_code, resp.content) self.assertIn('requires 2FA', resp.content) assert Authenticator.objects.filter(id=auth.id, ).exists() assert email_log.info.call_count == 0
def test_get_authenticator_details(self): interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse('sentry-api-0-user-authenticator-details', kwargs={ 'user_id': self.user.id, 'auth_id': auth.id, }) resp = self.client.get(url) assert resp.status_code == 200 assert resp.data['isEnrolled'] assert resp.data['id'] == "totp" assert resp.data['authId'] == six.text_type(auth.id) # should not have these because enrollment assert 'totp_secret' not in resp.data assert 'form' not in resp.data assert 'qrcode' not in resp.data
def test_2fa_settings_render_with_2fa(self): user = self.create_user('*****@*****.**') path = reverse('sentry-account-settings-2fa') self.login_as(user) TotpInterface().enroll(user) resp = self.client.get(path) assert resp.status_code == 200 self.assertTemplateUsed('sentry/account/twofactor.html') assert 'has_2fa' in resp.context assert resp.context['has_2fa'] is True self.assertNotContains(resp, 'this can only be managed if 2FA is enabled') self.assertContains(resp, '<span class="icon-trash">')
def test_user_has_2fa(self): user = self.create_user('*****@*****.**') assert Authenticator.objects.user_has_2fa(user) is False assert Authenticator.objects.filter(user=user).count() == 0 RecoveryCodeInterface().enroll(user) assert Authenticator.objects.user_has_2fa(user) is False assert Authenticator.objects.filter(user=user).count() == 1 TotpInterface().enroll(user) assert Authenticator.objects.user_has_2fa(user) is True assert Authenticator.objects.filter(user=user).count() == 2
def test_remove_2fa_SSO_deletes_lost_passswords(self): user = self.create_user('*****@*****.**') user.set_unusable_password() user.save() TotpInterface().enroll(user) path = reverse('sentry-account-settings-2fa-totp') self.login_as(user) LostPasswordHash.objects.create(user=user) resp = self.client.post(path, data={'remove': ''}) assert resp.status_code == 200 self.assertTemplateUsed('sentry/account/twofactor/remove.html') self.assertContains(resp, 'Do you want to remove the method?') self.assertNotContains(resp, 'Sentry account password') assert not LostPasswordHash.objects.filter(user=user).exists()
def test_get_authenticator_details(self): interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse( "sentry-api-0-user-authenticator-details", kwargs={ "user_id": self.user.id, "auth_id": auth.id }, ) resp = self.client.get(url) assert resp.status_code == 200 assert resp.data["isEnrolled"] assert resp.data["id"] == "totp" assert resp.data["authId"] == six.text_type(auth.id) # should not have these because enrollment assert "totp_secret" not in resp.data assert "form" not in resp.data assert "qrcode" not in resp.data
def test_get_authenticator_details(self): interface = TotpInterface() interface.enroll(self.user) auth = interface.authenticator url = reverse( 'sentry-api-0-user-authenticator-details', kwargs={ 'user_id': self.user.id, 'auth_id': auth.id, } ) resp = self.client.get(url) assert resp.status_code == 200 assert resp.data['isEnrolled'] assert resp.data['id'] == "totp" assert resp.data['authId'] == six.text_type(auth.id) # should not have these because enrollment assert 'totp_secret' not in resp.data assert 'form' not in resp.data assert 'qrcode' not in resp.data
def test_owner_can_set_org_2fa(self): org = self.create_organization(owner=self.owner) TotpInterface().enroll(self.owner) user_emails_without_2fa = self.add_2fa_users_to_org(org) with self.options({"system.url-prefix": "http://example.com"}), self.tasks(): self.assert_can_enable_org_2fa(org, self.owner) self.assert_2fa_email_equal(mail.outbox, user_emails_without_2fa) mail.outbox = [] with self.options({"system.url-prefix": "http://example.com"}), self.tasks(): response = self.api_disable_org_2fa(org, self.owner) assert response.status_code == 200 assert not Organization.objects.get(id=org.id).flags.require_2fa assert len(mail.outbox) == 0
def test_list_all_authenticators(self): user = self.create_user(email="*****@*****.**", is_superuser=True) self.login_as(user=user, superuser=True) url = reverse("sentry-api-0-user-authenticator-index", kwargs={"user_id": "me"}) resp = self.client.get(url, format="json") assert resp.status_code == 200 interface = [i for i in resp.data if i["id"] == "totp"][0] assert not interface["isEnrolled"] # Enroll in Totp - should still be listed TotpInterface().enroll(user) resp = self.client.get(url, format="json") assert resp.status_code == 200 interface = [i for i in resp.data if i["id"] == "totp"][0] assert interface["isEnrolled"]
def test_list_all_authenticators(self): user = self.create_user(email='*****@*****.**', is_superuser=True) self.login_as(user=user, superuser=True) url = reverse( 'sentry-api-0-user-authenticator-index', kwargs={'user_id': 'me'} ) resp = self.client.get(url, format='json') assert resp.status_code == 200 interface = [i for i in resp.data if i['id'] == "totp"][0] assert not interface['isEnrolled'] # Enroll in Totp - should still be listed TotpInterface().enroll(user) resp = self.client.get(url, format='json') assert resp.status_code == 200 interface = [i for i in resp.data if i['id'] == "totp"][0] assert interface['isEnrolled']
def test_various_options(self): org = self.create_organization(owner=self.user) initial = org.get_audit_log_data() AuditLogEntry.objects.filter(organization=org).delete() self.login_as(user=self.user) url = reverse( 'sentry-api-0-organization-details', kwargs={ 'organization_slug': org.slug, } ) data = { 'openMembership': False, 'isEarlyAdopter': True, 'allowSharedIssues': False, 'enhancedPrivacy': True, 'dataScrubber': True, 'dataScrubberDefaults': True, 'sensitiveFields': [u'password'], 'safeFields': [u'email'], 'storeCrashReports': True, 'scrubIPAddresses': True, 'scrapeJavaScript': False, 'defaultRole': 'owner', 'require2FA': True } # needed to set require2FA interface = TotpInterface() interface.enroll(self.user) assert Authenticator.objects.user_has_2fa(self.user) response = self.client.put(url, data=data) assert response.status_code == 200, response.content org = Organization.objects.get(id=org.id) assert initial != org.get_audit_log_data() assert org.flags.early_adopter assert not org.flags.allow_joinleave assert org.flags.disable_shared_issues assert org.flags.enhanced_privacy assert org.flags.require_2fa assert org.default_role == 'owner' options = {o.key: o.value for o in OrganizationOption.objects.filter( organization=org, )} assert options.get('sentry:require_scrub_defaults') assert options.get('sentry:require_scrub_data') assert options.get('sentry:require_scrub_ip_address') assert options.get('sentry:sensitive_fields') == ['password'] assert options.get('sentry:safe_fields') == ['email'] assert options.get('sentry:store_crash_reports') is True assert options.get('sentry:scrape_javascript') is False # log created log = AuditLogEntry.objects.get(organization=org) assert log.get_event_display() == 'org.edit' # org fields & flags assert u'to {}'.format(data['defaultRole']) in log.data['default_role'] assert u'to {}'.format(data['openMembership']) in log.data['allow_joinleave'] assert u'to {}'.format(data['isEarlyAdopter']) in log.data['early_adopter'] assert u'to {}'.format(data['enhancedPrivacy']) in log.data['enhanced_privacy'] assert u'to {}'.format(not data['allowSharedIssues']) in log.data['disable_shared_issues'] assert u'to {}'.format(data['require2FA']) in log.data['require_2fa'] # org options assert u'to {}'.format(data['dataScrubber']) in log.data['dataScrubber'] assert u'to {}'.format(data['dataScrubberDefaults']) in log.data['dataScrubberDefaults'] assert u'to {}'.format(data['sensitiveFields']) in log.data['sensitiveFields'] assert u'to {}'.format(data['safeFields']) in log.data['safeFields'] assert u'to {}'.format(data['scrubIPAddresses']) in log.data['scrubIPAddresses'] assert u'to {}'.format(data['scrapeJavaScript']) in log.data['scrapeJavaScript']
def _enroll_user_in_2fa(self): interface = TotpInterface() interface.enroll(self.user) self.assertTrue(Authenticator.objects.user_has_2fa(self.user))