Browse Source

chore(hybridcloud) Put 2fa view in control silo (#58730)

Put annotations on the 2fa views and write rudimentary tests for them as
there were no tests prior.

Refs HC-947
Mark Story 1 year ago
parent
commit
9a4911012e

+ 4 - 1
src/sentry/web/frontend/twofactor.py

@@ -11,9 +11,10 @@ from sentry import ratelimits as ratelimiter
 from sentry.auth.authenticators.sms import SMSRateLimitExceeded
 from sentry.auth.authenticators.u2f import U2fInterface
 from sentry.models.authenticator import Authenticator
+from sentry.services.hybrid_cloud.util import control_silo_function
 from sentry.utils import auth, json
 from sentry.web.forms.accounts import TwoFactorForm
-from sentry.web.frontend.base import BaseView
+from sentry.web.frontend.base import BaseView, control_silo_view
 from sentry.web.helpers import render_to_response
 
 COOKIE_NAME = "s2fai"
@@ -22,6 +23,7 @@ COOKIE_MAX_AGE = 60 * 60 * 24 * 31
 logger = logging.getLogger(__name__)
 
 
+@control_silo_view
 class TwoFactorAuthView(BaseView):
     auth_required = False
 
@@ -190,6 +192,7 @@ class TwoFactorAuthView(BaseView):
         )
 
 
+@control_silo_function
 def u2f_appid(request):
     facets = options.get("u2f.facets")
     if not facets:

+ 4 - 0
static/app/data/controlsiloUrlPatterns.ts

@@ -3,6 +3,7 @@
 const patterns: RegExp[] = [
   new RegExp('^remote/github/marketplace/purchase/$'),
   new RegExp('^docs/api/user/$'),
+  new RegExp('^_experiment/log_exposure/$'),
   new RegExp('^api/0/audit-logs/$'),
   new RegExp('^api/0/_admin/options/$'),
   new RegExp('^api/0/billingadmins/$'),
@@ -15,6 +16,7 @@ const patterns: RegExp[] = [
   new RegExp('^api/0/broadcasts/[^/]+/$'),
   new RegExp('^api/0/customers/[^/]+/subscription/usage-logs/$'),
   new RegExp('^api/0/customers/[^/]+/policies/$'),
+  new RegExp('^api/0/customers/[^/]+/migrate-google-domain/$'),
   new RegExp('^api/0/users/[^/]+/merge-accounts/$'),
   new RegExp('^api/0/policies/$'),
   new RegExp('^api/0/policies/[^/]+/$'),
@@ -25,6 +27,7 @@ const patterns: RegExp[] = [
   new RegExp('^api/0/promocodes/[^/]+/claimants/$'),
   new RegExp('^api/0/gdpr_request/$'),
   new RegExp('^api/0/sponsored_account_request/$'),
+  new RegExp('^api/0/migrate_to_hosted/$'),
   new RegExp('^api/0/sponsored_education_account/$'),
   new RegExp('^api/0/organizations/[^/]+/broadcasts/$'),
   new RegExp('^api/0/auth-details/$'),
@@ -122,6 +125,7 @@ const patterns: RegExp[] = [
   new RegExp('^auth/login/[^/]+/$'),
   new RegExp('^auth/channel/[^/]+/[^/]+/$'),
   new RegExp('^auth/link/[^/]+/$'),
+  new RegExp('^auth/2fa/$'),
   new RegExp('^auth/sso/$'),
   new RegExp('^auth/register/$'),
   new RegExp('^avatar/[^/]+/$'),

+ 74 - 0
tests/sentry/web/frontend/test_twofactor.py

@@ -0,0 +1,74 @@
+from time import time
+from unittest import mock
+
+from sentry.auth.authenticators.totp import TotpInterface
+from sentry.testutils.cases import TestCase
+from sentry.testutils.silo import control_silo_test
+
+
+@control_silo_test(stable=True)
+class TwoFactorTest(TestCase):
+    def test_not_pending_2fa(self):
+        resp = self.client.get("/auth/2fa/")
+        assert resp.status_code == 302
+        assert resp["Location"] == "/auth/login/"
+
+    def test_no_2fa_configured(self):
+        user = self.create_user()
+        self.login_as(user)
+
+        self.session["_pending_2fa"] = [user.id, time() - 2]
+        self.save_session()
+
+        resp = self.client.get("/auth/2fa/", follow=True)
+        assert resp.redirect_chain == [
+            ("/auth/login/", 302),
+            ("/organizations/new/", 302),
+        ]
+
+    def test_otp_challenge(self):
+        user = self.create_user()
+        interface = TotpInterface()
+        interface.enroll(user)
+
+        self.login_as(user)
+        self.session["_pending_2fa"] = [user.id, time() - 2]
+        self.save_session()
+
+        resp = self.client.get("/auth/2fa/")
+        assert resp.status_code == 200
+        self.assertTemplateUsed("sentry/twofactor.html")
+        assert "provide the access code" in resp.content.decode("utf8")
+
+    @mock.patch("sentry.auth.authenticators.TotpInterface.validate_otp", return_value=None)
+    @mock.patch("time.sleep")
+    def test_otp_submit_error(self, mock_sleep, mock_validate):
+        user = self.create_user()
+        interface = TotpInterface()
+        interface.enroll(user)
+
+        self.login_as(user)
+        self.session["_pending_2fa"] = [user.id, time() - 2]
+        self.save_session()
+
+        resp = self.client.post("/auth/2fa/", data={"otp": "123456"}, follow=True)
+        assert mock_validate.called
+        assert resp.status_code == 200
+        assert "Invalid confirmation code" in resp.content.decode("utf8")
+
+    @mock.patch("sentry.auth.authenticators.TotpInterface.validate_otp", return_value=True)
+    def test_otp_submit_success(self, mock_validate):
+        user = self.create_user()
+        interface = TotpInterface()
+        interface.enroll(user)
+
+        self.login_as(user)
+        self.session["_pending_2fa"] = [user.id, time() - 2]
+        self.save_session()
+
+        resp = self.client.post("/auth/2fa/", data={"otp": "123456"}, follow=True)
+        assert mock_validate.called
+        assert resp.redirect_chain == [
+            ("/auth/login/", 302),
+            ("/organizations/new/", 302),
+        ]