dograh/api/tests/test_auth_routes.py

82 lines
2.2 KiB
Python
Raw Normal View History

from types import SimpleNamespace
from fastapi import FastAPI
from fastapi.testclient import TestClient
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
import api.routes.auth as auth_routes
from api.routes.auth import router
from api.services.auth import depends as auth_depends
from api.services.auth.depends import get_user
def _make_test_app() -> FastAPI:
app = FastAPI()
app.include_router(router)
return app
def test_stack_mode_hides_email_password_auth_routes(monkeypatch):
monkeypatch.setattr(auth_depends, "AUTH_PROVIDER", "stack")
client = TestClient(_make_test_app())
signup_response = client.post(
"/auth/signup",
json={
"email": "user@example.com",
"password": "password123",
"name": "User",
},
)
login_response = client.post(
"/auth/login",
json={
"email": "user@example.com",
"password": "password123",
},
)
assert signup_response.status_code == 404
assert signup_response.json() == {"detail": "Not found"}
assert login_response.status_code == 404
assert login_response.json() == {"detail": "Not found"}
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
def test_signup_disabled_returns_403(monkeypatch):
monkeypatch.setattr(auth_routes, "ENABLE_SIGNUP", False)
client = TestClient(_make_test_app())
response = client.post(
"/auth/signup",
json={
"email": "user@example.com",
"password": "password123",
"name": "User",
},
)
assert response.status_code == 403
assert response.json() == {"detail": "Signup is disabled"}
def test_stack_mode_keeps_current_user_route_available(monkeypatch):
monkeypatch.setattr(auth_depends, "AUTH_PROVIDER", "stack")
app = _make_test_app()
app.dependency_overrides[get_user] = lambda: SimpleNamespace(
id=7,
email="user@example.com",
selected_organization_id=42,
provider_id="stack-user-1",
)
client = TestClient(app)
response = client.get("/auth/me")
assert response.status_code == 200
assert response.json() == {
"id": 7,
"email": "user@example.com",
"name": None,
"organization_id": 42,
"provider_id": "stack-user-1",
}