Enable password self-registration and keep SSO off.
Public sign-up was closed, and the live Google and Microsoft apps would have become sign-up buttons if the same flag turned both on.
This commit is contained in:
1 parent
4fc2b87bb4
commit
6a10f9778d
9 files changed
+90
-14
No files matched your search
+5
-2
@@ -39,8 +39,11 @@ EMAIL_USE_TLS=true
|
|||||||
# Captcha (optional local)
|
# Captcha (optional local)
|
||||||
CAPTCHA_SECRET_KEY=
|
CAPTCHA_SECRET_KEY=
|
||||||
|
|
||||||
# Self-serve sign-up (default false — set true to allow /user/create/)
|
# Password self-serve sign-up (default true). Set false to close /user/create/.
|
||||||
ENABLE_ACCOUNT_REGISTRATION=false
|
ENABLE_ACCOUNT_REGISTRATION=true
|
||||||
|
# Browser SSO. Leave false until Google / Microsoft sign-in should be public.
|
||||||
|
# Client ids below can still be set for Drive linking.
|
||||||
|
ENABLE_SSO=false
|
||||||
|
|
||||||
# OAuth SSO — Google / Microsoft (#24). Leave blank to hide SSO buttons.
|
# OAuth SSO — Google / Microsoft (#24). Leave blank to hide SSO buttons.
|
||||||
# Redirect URIs (register in each IdP console):
|
# Redirect URIs (register in each IdP console):
|
||||||
|
|||||||
+7
-3
@@ -59,9 +59,13 @@ EMAIL_USE_TLS=true
|
|||||||
# Captcha
|
# Captcha
|
||||||
CAPTCHA_SECRET_KEY=replace-with-captcha-secret
|
CAPTCHA_SECRET_KEY=replace-with-captcha-secret
|
||||||
|
|
||||||
# Self-serve account registration (sign-up). Keep false until ready to open
|
# Password self-serve sign-up. Default true. Set false in
|
||||||
# public registration; set true in chat_backend_prod.env / chat_backend_beta.env.
|
# chat_backend_prod.env / chat_backend_beta.env to close public registration.
|
||||||
ENABLE_ACCOUNT_REGISTRATION=false
|
# An explicit false in that secret overrides the code default.
|
||||||
|
ENABLE_ACCOUNT_REGISTRATION=true
|
||||||
|
# Browser SSO stays off. OAuth client ids below are for Drive linking until
|
||||||
|
# this is set true.
|
||||||
|
ENABLE_SSO=false
|
||||||
|
|
||||||
# OAuth SSO — Google / Microsoft (#24). Never commit real secrets.
|
# OAuth SSO — Google / Microsoft (#24). Never commit real secrets.
|
||||||
# Register redirect URIs:
|
# Register redirect URIs:
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ with `COMPOSE_DATABASE_URL` if needed.
|
|||||||
| `SEARXNG_BASE_URL` | `http://127.0.0.1:8088` | yes if using SearxNG | Self-hosted SearxNG JSON API |
|
| `SEARXNG_BASE_URL` | `http://127.0.0.1:8088` | yes if using SearxNG | Self-hosted SearxNG JSON API |
|
||||||
| `EMAIL_HOST_*` | empty | yes (prod/beta) | SMTP2GO |
|
| `EMAIL_HOST_*` | empty | yes (prod/beta) | SMTP2GO |
|
||||||
| `CAPTCHA_SECRET_KEY` | empty | recommended | |
|
| `CAPTCHA_SECRET_KEY` | empty | recommended | |
|
||||||
| `ENABLE_ACCOUNT_REGISTRATION` | `false` | optional | Self-serve sign-up; keep false until ready |
|
| `ENABLE_ACCOUNT_REGISTRATION` | `true` | optional | Password self-serve sign-up. Set `false` to close it |
|
||||||
|
| `ENABLE_SSO` | `false` | optional | Google / Microsoft sign-in and sign-up. Drive linking does not need this |
|
||||||
| `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` / `STRIPE_WEBHOOK_SECRET` | empty | yes for billing | Stripe API + webhook (`monetization` app) |
|
| `STRIPE_SECRET_KEY` / `STRIPE_PUBLISHABLE_KEY` / `STRIPE_WEBHOOK_SECRET` | empty | yes for billing | Stripe API + webhook (`monetization` app) |
|
||||||
| `APPLE_IAP_BUNDLE_ID` | `ai.hesychia.chat` | yes for iOS IAP | Must match the signed StoreKit transaction |
|
| `APPLE_IAP_BUNDLE_ID` | `ai.hesychia.chat` | yes for iOS IAP | Must match the signed StoreKit transaction |
|
||||||
| `GOOGLE_PLAY_PACKAGE_NAME` | `ai.hesychia.chat` | yes for Play IAP | Android application id |
|
| `GOOGLE_PLAY_PACKAGE_NAME` | `ai.hesychia.chat` | yes for Play IAP | Android application id |
|
||||||
|
|||||||
@@ -459,8 +459,8 @@ def resolve_user_from_profile(
|
|||||||
upsert_identity(email_user, profile)
|
upsert_identity(email_user, profile)
|
||||||
return email_user, False
|
return email_user, False
|
||||||
|
|
||||||
# New account path
|
# New account path. Password registration does not enable SSO sign-up.
|
||||||
allow_create = settings.ENABLE_ACCOUNT_REGISTRATION
|
allow_create = settings.ENABLE_ACCOUNT_REGISTRATION and settings.ENABLE_SSO
|
||||||
if intent == "signup" and not allow_create:
|
if intent == "signup" and not allow_create:
|
||||||
raise OAuthError("registration_disabled", "Account registration is disabled.")
|
raise OAuthError("registration_disabled", "Account registration is disabled.")
|
||||||
if intent == "login" and not allow_create:
|
if intent == "login" and not allow_create:
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ OAUTH_SETTINGS = {
|
|||||||
"FRONTEND_BASE_URL": "http://frontend.test",
|
"FRONTEND_BASE_URL": "http://frontend.test",
|
||||||
"OAUTH_CALLBACK_BASE_URL": "http://backend.test",
|
"OAUTH_CALLBACK_BASE_URL": "http://backend.test",
|
||||||
"ENABLE_ACCOUNT_REGISTRATION": True,
|
"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"]["google"])
|
||||||
self.assertTrue(response.data["oauth"]["microsoft"])
|
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="")
|
@override_settings(GOOGLE_OAUTH_CLIENT_ID="", GOOGLE_OAUTH_CLIENT_SECRET="")
|
||||||
def test_hides_unconfigured_provider(self):
|
def test_hides_unconfigured_provider(self):
|
||||||
response = self.client.get(reverse("public_settings"))
|
response = self.client.get(reverse("public_settings"))
|
||||||
@@ -83,6 +92,15 @@ class OAuthStartTestCase(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
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)
|
@override_settings(ENABLE_ACCOUNT_REGISTRATION=False)
|
||||||
def test_signup_intent_blocked_when_registration_disabled(self):
|
def test_signup_intent_blocked_when_registration_disabled(self):
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@@ -180,6 +198,17 @@ class OAuthCallbackTestCase(APITestCase):
|
|||||||
params = parse_qs(urlparse(response["Location"]).query)
|
params = parse_qs(urlparse(response["Location"]).query)
|
||||||
self.assertEqual(params["error"], ["email_missing"])
|
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")
|
@patch("chat_backend.views_oauth.exchange_code_for_profile")
|
||||||
@override_settings(ENABLE_ACCOUNT_REGISTRATION=False)
|
@override_settings(ENABLE_ACCOUNT_REGISTRATION=False)
|
||||||
def test_callback_login_without_account_when_registration_off(self, mock_exchange):
|
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)
|
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):
|
def test_link_drive_authenticated_redirects_with_drive_scope_and_state(self):
|
||||||
self.client.force_authenticate(user=self.user)
|
self.client.force_authenticate(user=self.user)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
|||||||
@@ -115,9 +115,27 @@ class PublicSettingsTestCase(APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(response.data["enable_account_registration"], False)
|
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):
|
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(
|
response = self.client.post(
|
||||||
reverse("create_user"),
|
reverse("create_user"),
|
||||||
{"email": "new@example.com", "password": "securepass1"},
|
{"email": "new@example.com", "password": "securepass1"},
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class PublicWaitlistView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
class CustomUserCreate(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,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
authentication_classes = ()
|
authentication_classes = ()
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ class OAuthStartView(APIView):
|
|||||||
},
|
},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
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:
|
if intent == "signup" and not settings.ENABLE_ACCOUNT_REGISTRATION:
|
||||||
return Response(
|
return Response(
|
||||||
{"detail": "Account registration is disabled."},
|
{"detail": "Account registration is disabled."},
|
||||||
@@ -184,6 +189,8 @@ class OAuthCallbackView(APIView):
|
|||||||
return self._handle_drive_link_callback(
|
return self._handle_drive_link_callback(
|
||||||
request, provider=provider, code=code, state_data=state_data
|
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:
|
try:
|
||||||
redirect_uri = _callback_redirect_uri(request, provider)
|
redirect_uri = _callback_redirect_uri(request, provider)
|
||||||
@@ -261,5 +268,7 @@ class OAuthCallbackView(APIView):
|
|||||||
|
|
||||||
|
|
||||||
def oauth_public_flags() -> dict:
|
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()
|
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)
|
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
|
# Password self-serve sign-up. On by default (#80). Set false in the
|
||||||
# control-node secret (chat_backend_<env>.env) when ready for public sign-up.
|
# control-node secret (chat_backend_<env>.env) to close public registration.
|
||||||
ENABLE_ACCOUNT_REGISTRATION = env_bool("ENABLE_ACCOUNT_REGISTRATION", False)
|
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
|
# OAuth SSO (Google / Microsoft) — #24
|
||||||
|
|||||||
Reference in new issue
Block a user