Enable password self-registration and keep SSO off (#81)
## Summary - Closes #80. - Password self-registration is on by default (`ENABLE_ACCOUNT_REGISTRATION`). `/user/create/` issues tokens and `needs_checkout`, and the web sign-up page already redirects into Stripe Checkout. - Browser SSO stays off (`ENABLE_SSO`, default false). Live Google and Microsoft client ids stay available for Drive linking, but public settings no longer advertise them, and login/signup OAuth start and callback are rejected. Stripe path checked before this change: Checkout builds a subscription session (Founders at $10/month, or `STRIPE_PRICE_ID`), stores a draft invoice, and signed `checkout.session.completed` / `invoice.paid` webhooks grant the plan. Checkout, webhook, portal, registration, and OAuth tests passed. ## Deploy note If `chat_backend_prod.env` or `chat_backend_beta.env` on the control node still sets `ENABLE_ACCOUNT_REGISTRATION=false`, that line overrides this default. Set it to `true` or remove it, and leave `ENABLE_SSO` unset or `false`. ## Test plan - [ ] `GET /api/public/settings/` returns `enable_account_registration: true` and `oauth.google` / `oauth.microsoft` false. - [ ] Sign-up form creates an account and redirects to Stripe Checkout. No Google or Microsoft button. - [ ] Checkout success grants the Founders plan after the webhook. - [ ] `/api/auth/oauth/google/start/?intent=signup` returns 403. Drive connect still starts. - [ ] Setting `ENABLE_ACCOUNT_REGISTRATION=false` closes `/user/create/` again. Reviewed-on: #81
This commit was merged in pull request #81.
This commit is contained in:
1 parent
4fc2b87bb4
commit
32f57c2376
9 files changed
+90
-14
No files matched your search
@@ -459,8 +459,8 @@ def resolve_user_from_profile(
|
||||
upsert_identity(email_user, profile)
|
||||
return email_user, False
|
||||
|
||||
# New account path
|
||||
allow_create = settings.ENABLE_ACCOUNT_REGISTRATION
|
||||
# New account path. Password registration does not enable SSO sign-up.
|
||||
allow_create = settings.ENABLE_ACCOUNT_REGISTRATION and settings.ENABLE_SSO
|
||||
if intent == "signup" and not allow_create:
|
||||
raise OAuthError("registration_disabled", "Account registration is disabled.")
|
||||
if intent == "login" and not allow_create:
|
||||
|
||||
@@ -24,6 +24,7 @@ OAUTH_SETTINGS = {
|
||||
"FRONTEND_BASE_URL": "http://frontend.test",
|
||||
"OAUTH_CALLBACK_BASE_URL": "http://backend.test",
|
||||
"ENABLE_ACCOUNT_REGISTRATION": True,
|
||||
"ENABLE_SSO": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +54,14 @@ class PublicSettingsOAuthTestCase(APITestCase):
|
||||
self.assertTrue(response.data["oauth"]["google"])
|
||||
self.assertTrue(response.data["oauth"]["microsoft"])
|
||||
|
||||
@override_settings(ENABLE_SSO=False)
|
||||
def test_hides_oauth_when_sso_disabled(self):
|
||||
response = self.client.get(reverse("public_settings"))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(response.data["enable_account_registration"])
|
||||
self.assertFalse(response.data["oauth"]["google"])
|
||||
self.assertFalse(response.data["oauth"]["microsoft"])
|
||||
|
||||
@override_settings(GOOGLE_OAUTH_CLIENT_ID="", GOOGLE_OAUTH_CLIENT_SECRET="")
|
||||
def test_hides_unconfigured_provider(self):
|
||||
response = self.client.get(reverse("public_settings"))
|
||||
@@ -83,6 +92,15 @@ class OAuthStartTestCase(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@override_settings(ENABLE_SSO=False)
|
||||
def test_login_and_signup_blocked_when_sso_disabled(self):
|
||||
for intent in ("login", "signup"):
|
||||
response = self.client.get(
|
||||
reverse("oauth_start", kwargs={"provider": "google"}),
|
||||
{"intent": intent},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
@override_settings(ENABLE_ACCOUNT_REGISTRATION=False)
|
||||
def test_signup_intent_blocked_when_registration_disabled(self):
|
||||
response = self.client.get(
|
||||
@@ -180,6 +198,17 @@ class OAuthCallbackTestCase(APITestCase):
|
||||
params = parse_qs(urlparse(response["Location"]).query)
|
||||
self.assertEqual(params["error"], ["email_missing"])
|
||||
|
||||
@patch("chat_backend.views_oauth.exchange_code_for_profile")
|
||||
@override_settings(ENABLE_SSO=False)
|
||||
def test_callback_signup_rejected_when_sso_disabled(self, mock_exchange):
|
||||
mock_exchange.return_value = _google_profile()
|
||||
|
||||
response = self._callback(intent="signup")
|
||||
params = parse_qs(urlparse(response["Location"]).query)
|
||||
self.assertEqual(params["error"], ["sso_disabled"])
|
||||
self.assertEqual(CustomUser.objects.count(), 0)
|
||||
mock_exchange.assert_not_called()
|
||||
|
||||
@patch("chat_backend.views_oauth.exchange_code_for_profile")
|
||||
@override_settings(ENABLE_ACCOUNT_REGISTRATION=False)
|
||||
def test_callback_login_without_account_when_registration_off(self, mock_exchange):
|
||||
@@ -256,6 +285,15 @@ class OAuthStartDriveLinkTestCase(APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@override_settings(ENABLE_SSO=False)
|
||||
def test_link_drive_still_starts_when_sso_disabled(self):
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.get(
|
||||
reverse("oauth_start", kwargs={"provider": "google"}),
|
||||
{"intent": "link_drive"},
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
|
||||
|
||||
def test_link_drive_authenticated_redirects_with_drive_scope_and_state(self):
|
||||
self.client.force_authenticate(user=self.user)
|
||||
response = self.client.get(
|
||||
|
||||
@@ -115,15 +115,33 @@ class PublicSettingsTestCase(APITestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["enable_account_registration"], False)
|
||||
|
||||
def test_registration_on_and_sso_hidden_by_default(self):
|
||||
response = self.client.get(reverse("public_settings"))
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["enable_account_registration"], True)
|
||||
self.assertEqual(response.data["oauth"], {"google": False, "microsoft": False})
|
||||
|
||||
|
||||
class UserCreateTestCase(APITestCase):
|
||||
def test_registration_disabled_by_default(self):
|
||||
def test_registration_enabled_by_default(self):
|
||||
response = self.client.post(
|
||||
reverse("create_user"),
|
||||
{"email": "new@example.com", "password": "securepass1"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertTrue(CustomUser.objects.filter(email="new@example.com").exists())
|
||||
|
||||
def test_registration_disabled_when_setting_off(self):
|
||||
with self.settings(ENABLE_ACCOUNT_REGISTRATION=False):
|
||||
response = self.client.post(
|
||||
reverse("create_user"),
|
||||
{"email": "new@example.com", "password": "securepass1"},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(CustomUser.objects.count(), 0)
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class PublicWaitlistView(APIView):
|
||||
|
||||
|
||||
class CustomUserCreate(APIView):
|
||||
"""Self-serve registration. Gated by ENABLE_ACCOUNT_REGISTRATION (default off)."""
|
||||
"""Password self-serve registration. Gated by ENABLE_ACCOUNT_REGISTRATION."""
|
||||
|
||||
permission_classes = (permissions.AllowAny,)
|
||||
authentication_classes = ()
|
||||
|
||||
@@ -102,6 +102,11 @@ class OAuthStartView(APIView):
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if intent in {"login", "signup"} and not settings.ENABLE_SSO:
|
||||
return Response(
|
||||
{"detail": "Single sign-on is not enabled."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
if intent == "signup" and not settings.ENABLE_ACCOUNT_REGISTRATION:
|
||||
return Response(
|
||||
{"detail": "Account registration is disabled."},
|
||||
@@ -184,6 +189,8 @@ class OAuthCallbackView(APIView):
|
||||
return self._handle_drive_link_callback(
|
||||
request, provider=provider, code=code, state_data=state_data
|
||||
)
|
||||
if not settings.ENABLE_SSO:
|
||||
return _redirect_error("sso_disabled", "Single sign-on is not enabled.")
|
||||
|
||||
try:
|
||||
redirect_uri = _callback_redirect_uri(request, provider)
|
||||
@@ -261,5 +268,7 @@ class OAuthCallbackView(APIView):
|
||||
|
||||
|
||||
def oauth_public_flags() -> dict:
|
||||
"""Feature flags for /public/settings/."""
|
||||
"""Feature flags for /public/settings/. SSO buttons stay hidden until ENABLE_SSO."""
|
||||
if not settings.ENABLE_SSO:
|
||||
return {"google": False, "microsoft": False}
|
||||
return configured_providers()
|
||||
@@ -381,9 +381,12 @@ AGENT_FETCH_URL_MAX_BYTES = int(
|
||||
env("AGENT_FETCH_URL_MAX_BYTES", str(2 * 1024 * 1024)) or str(2 * 1024 * 1024)
|
||||
)
|
||||
|
||||
# Self-serve account registration (sign-up page). Default off — enable via
|
||||
# control-node secret (chat_backend_<env>.env) when ready for public sign-up.
|
||||
ENABLE_ACCOUNT_REGISTRATION = env_bool("ENABLE_ACCOUNT_REGISTRATION", False)
|
||||
# Password self-serve sign-up. On by default (#80). Set false in the
|
||||
# control-node secret (chat_backend_<env>.env) to close public registration.
|
||||
ENABLE_ACCOUNT_REGISTRATION = env_bool("ENABLE_ACCOUNT_REGISTRATION", True)
|
||||
# Browser SSO (Google / Microsoft sign-in and sign-up). Off until explicitly
|
||||
# enabled. OAuth client ids may still be set for Drive linking (#47).
|
||||
ENABLE_SSO = env_bool("ENABLE_SSO", False)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OAuth SSO (Google / Microsoft) — #24
|
||||
|
||||
Reference in new issue
Block a user