Status
Decision required from the product owner before any implementation. This issue stays open
with the decision label until the owner picks one of the options below. Nothing here is a
recommendation, and no implementation should start while the decision is open
(_docs/PROCESS.md, decision label).
Priority P0: whatever the owner decides, the current state is self-contradictory — a settings
flag that reads "registration disabled" while a public form on every page's masthead registers
accounts — and the two readings imply opposite fixes.
Normative context
_docs/specs/01-platform-architecture.md § "Member signup, profile, Slack, and course
registration" specifies email/password signup as conditional: "Email/password signup, when
enabled, collects only email, its credential, and the required account/privacy
acknowledgement." The enabling condition is defined nowhere.
_docs/specs/07-security-privacy-operations.md § "Edge and application abuse controls": the
per-source-IP WAF rate rule for signup/login/profile/Slack/registration (60 requests per five
minutes) starts in count mode.
- Commit 8c69a93 ("Bring the account entrance into the design system") deliberately left
openness unchanged and deferred it to this issue.
Verified behavior at current main (9603342)
Observed by live request under website.settings.local. The flag is inherited from
website/settings/base.py; no settings module (development, local, local_review,
test, production) overrides it and no environment variable reads it, so it is False
everywhere, dev included.
GET /accounts/signup/ returns 200 and renders the design 5a signup page
(accounts/templates/account/signup.html, added in 8c69a93): three provider buttons, then
the email/password card with email, password1, password2, the CSRF token, and next
passthrough.
POST /accounts/signup/ with a fresh email and matching passwords returns 302 to / with
the session logged in. It creates an is_active=True CustomUser with
identity_state="legacy", normalized_email set, and a primary EmailAddress row with
verified=False. No verification email is ever sent
(ACCOUNT_EMAIL_VERIFICATION = "none", website/settings/base.py:183). Verified by
request; the probe user was deleted afterwards.
- Entrances: the masthead "Sign up free" button on every design 5a page
(templates/core/_site_shell_head.html:275) and both homepage CTAs "Create your free
account" (templates/core/home.html:650,1069) link to {% url 'account_signup' %}, and
accounts/tests/test_account_entrance_design.py exercises the open form as expected
behavior.
Why ACCOUNT_ALLOW_REGISTRATION = False does not close it
website/settings/base.py:189 sets the flag (and course_management/settings.py:380 repeats
it under the comment # Disable sign-up form — the copied course-platform intent). That name
is not a django-allauth setting. The only reader in this repository is
accounts/identity_inventory.py:213, which reports it as account_registration_enabled
inventory metadata — so the inventory today claims registration is disabled while the form is
open.
- allauth decides local signup openness through
ACCOUNT_ADAPTER.is_open_for_signup().
ACCOUNT_ADAPTER is never set anywhere (only SOCIALACCOUNT_ADAPTER appears,
website/settings/base.py:190), so allauth's DefaultAccountAdapter applies; verified live:
it answers is_open_for_signup() → True.
- The social path is gated independently and fail-closed:
accounts.auth.ConsolidatingSocialAccountAdapter (accounts/auth.py:258) returns False
from is_open_for_signup (accounts/auth.py:261-264), and pre_social_login only links a
provider onto exactly one existing durable account. Social providers cannot register new
accounts whether the flag is True or False. The flag gates nothing at all.
Net state: social signup closed, local signup open — the opposite of what the settings read
like at a glance.
Security, abuse, and SEO implications
Keeping local signup open (today's behavior):
- Any anonymous visitor or script can create unlimited active user rows with arbitrary
unverified emails: no captcha, no email verification, and the WAF signup rate rule is still
in count mode (spec 07).
- Every row lands in the Django admin user list and emits an
auth.user_created observability
event (accounts/signals.py:55-66), polluting operational views.
ACCOUNT_UNIQUE_EMAIL = True (website/settings/base.py:186) means a squatted address
blocks the real owner's later local signup with that email.
- Squatted rows also collide with the durable-identity path:
_activate_verified_identity
(accounts/auth.py:211-231) refuses to activate a verified identity while any other user
holds the same normalized_email (_has_unresolved_email_collision,
accounts/auth.py:202-210), so a pre-registered row can deny the legitimate owner's social
login with normalized_email_conflict. (The social adapter's candidate lookup requires a
verified EmailAddress (accounts/auth.py:191-199), so an unverified squatting row cannot
by itself capture a social login — the exposure is denial, not takeover.)
- SEO: negligible direct effect; an open auth form is a normal crawlable page.
Closing local signup behind the flag:
- Removes every vector above.
- New members can no longer self-serve an account by any path, because social login is
fail-closed onto existing accounts. Account creation becomes operator/invite-driven until a
deliberate path is built. The homepage's primary CTA ("Create your free account") and the
masthead "Sign up free" button stop matching reality and must be redirected or reworded —
they are on every page.
- No design work is needed: the closed state already has a styled page
(accounts/templates/account/signup_closed.html, from 8c69a93).
Options for the owner
Option A — close local signup (make the flag mean what it says)
Requires:
- Set
ACCOUNT_ADAPTER to an adapter whose is_open_for_signup() returns
settings.ACCOUNT_ALLOW_REGISTRATION; allauth then routes GET and POST to the already-styled
signup_closed.html.
- Decide the masthead "Sign up free" and homepage CTA destinations (e.g.
/accounts/login/,
or keep them pointing at /accounts/signup/ to show the closed notice) and update
templates/core/_site_shell_head.html and templates/core/home.html.
- Make
accounts/identity_inventory.py report the enforced value (or drop the field) so the
inventory cannot again claim closed-while-open.
- Update
accounts/tests/test_account_entrance_design.py and related tests; add tests that GET
shows the closed notice and POST creates no user.
Option B — keep local signup open, deliberately
Requires:
- Set
ACCOUNT_ALLOW_REGISTRATION = True (or delete the flag and make the inventory report the
adapter's actual answer) so no dead, misleading setting remains.
- Record the decision in
_docs/specs/01-platform-architecture.md § "Member signup, profile,
Slack, and course registration" (the "when enabled" clause needs its condition defined).
- Decide and record the abuse posture: keep
ACCOUNT_EMAIL_VERIFICATION = "none" or move to
verification; confirm the WAF signup rate rule promotion plan (spec 07).
- Treat
/accounts/signup/ as a first-class open page in the accessibility registry and core
browser/smoke coverage (partially exists via test_account_entrance_design.py).
Option C — something else
E.g. invite-only signup, email-verified-only signup, or closing local signup while opening a
deliberate provider signup path. The owner should state the intended behavior in this issue;
the PM will re-groom into implementation criteria at that point.
Non-goals (all options)
- No changes to the social consolidation adapter or its fail-closed semantics
(accounts/auth.py).
- No new signup styling or design work — the open and closed pages are both design 5a already.
- No password reset, login, MFA, or account-deletion changes.
- No WAF/Terraform changes in this issue; abuse-control rollout stays owned by spec 07.
Dependencies
- None technical. This decision blocks any work that assumes a signup posture (growth,
invitations, cohort onboarding flows).
Acceptance criteria
Apply to whichever option the owner picks. Nothing may be checked before the decision is
recorded in this issue by the owner.
Browser scenarios
- A signed-out visitor follows the masthead "Sign up free" button and observes behavior
consistent with the recorded decision, at desktop and mobile widths.
- A signed-out visitor submits the signup form with valid and invalid input and observes the
decided outcome, with errors announced accessibly wherever the form remains.
- An operator reads the identity inventory output and sees a registration value that matches
what a browser can actually do at /accounts/signup/.
Status
Decision required from the product owner before any implementation. This issue stays open
with the
decisionlabel until the owner picks one of the options below. Nothing here is arecommendation, and no implementation should start while the decision is open
(
_docs/PROCESS.md,decisionlabel).Priority P0: whatever the owner decides, the current state is self-contradictory — a settings
flag that reads "registration disabled" while a public form on every page's masthead registers
accounts — and the two readings imply opposite fixes.
Normative context
_docs/specs/01-platform-architecture.md§ "Member signup, profile, Slack, and courseregistration" specifies email/password signup as conditional: "Email/password signup, when
enabled, collects only email, its credential, and the required account/privacy
acknowledgement." The enabling condition is defined nowhere.
_docs/specs/07-security-privacy-operations.md§ "Edge and application abuse controls": theper-source-IP WAF rate rule for signup/login/profile/Slack/registration (60 requests per five
minutes) starts in count mode.
openness unchanged and deferred it to this issue.
Verified behavior at current
main(9603342)Observed by live request under
website.settings.local. The flag is inherited fromwebsite/settings/base.py; no settings module (development,local,local_review,test,production) overrides it and no environment variable reads it, so it isFalseeverywhere, dev included.
GET /accounts/signup/returns 200 and renders the design 5a signup page(
accounts/templates/account/signup.html, added in 8c69a93): three provider buttons, thenthe email/password card with
email,password1,password2, the CSRF token, andnextpassthrough.
POST /accounts/signup/with a fresh email and matching passwords returns 302 to/withthe session logged in. It creates an
is_active=TrueCustomUserwithidentity_state="legacy",normalized_emailset, and a primaryEmailAddressrow withverified=False. No verification email is ever sent(
ACCOUNT_EMAIL_VERIFICATION = "none",website/settings/base.py:183). Verified byrequest; the probe user was deleted afterwards.
(
templates/core/_site_shell_head.html:275) and both homepage CTAs "Create your freeaccount" (
templates/core/home.html:650,1069) link to{% url 'account_signup' %}, andaccounts/tests/test_account_entrance_design.pyexercises the open form as expectedbehavior.
Why
ACCOUNT_ALLOW_REGISTRATION = Falsedoes not close itwebsite/settings/base.py:189sets the flag (andcourse_management/settings.py:380repeatsit under the comment
# Disable sign-up form— the copied course-platform intent). That nameis not a django-allauth setting. The only reader in this repository is
accounts/identity_inventory.py:213, which reports it asaccount_registration_enabledinventory metadata — so the inventory today claims registration is disabled while the form is
open.
ACCOUNT_ADAPTER.is_open_for_signup().ACCOUNT_ADAPTERis never set anywhere (onlySOCIALACCOUNT_ADAPTERappears,website/settings/base.py:190), so allauth'sDefaultAccountAdapterapplies; verified live:it answers
is_open_for_signup() → True.accounts.auth.ConsolidatingSocialAccountAdapter(accounts/auth.py:258) returnsFalsefrom
is_open_for_signup(accounts/auth.py:261-264), andpre_social_loginonly links aprovider onto exactly one existing durable account. Social providers cannot register new
accounts whether the flag is
TrueorFalse. The flag gates nothing at all.Net state: social signup closed, local signup open — the opposite of what the settings read
like at a glance.
Security, abuse, and SEO implications
Keeping local signup open (today's behavior):
unverified emails: no captcha, no email verification, and the WAF signup rate rule is still
in count mode (spec 07).
auth.user_createdobservabilityevent (
accounts/signals.py:55-66), polluting operational views.ACCOUNT_UNIQUE_EMAIL = True(website/settings/base.py:186) means a squatted addressblocks the real owner's later local signup with that email.
_activate_verified_identity(
accounts/auth.py:211-231) refuses to activate a verified identity while any other userholds the same
normalized_email(_has_unresolved_email_collision,accounts/auth.py:202-210), so a pre-registered row can deny the legitimate owner's sociallogin with
normalized_email_conflict. (The social adapter's candidate lookup requires averified
EmailAddress(accounts/auth.py:191-199), so an unverified squatting row cannotby itself capture a social login — the exposure is denial, not takeover.)
Closing local signup behind the flag:
fail-closed onto existing accounts. Account creation becomes operator/invite-driven until a
deliberate path is built. The homepage's primary CTA ("Create your free account") and the
masthead "Sign up free" button stop matching reality and must be redirected or reworded —
they are on every page.
(
accounts/templates/account/signup_closed.html, from 8c69a93).Options for the owner
Option A — close local signup (make the flag mean what it says)
Requires:
ACCOUNT_ADAPTERto an adapter whoseis_open_for_signup()returnssettings.ACCOUNT_ALLOW_REGISTRATION; allauth then routes GET and POST to the already-styledsignup_closed.html./accounts/login/,or keep them pointing at
/accounts/signup/to show the closed notice) and updatetemplates/core/_site_shell_head.htmlandtemplates/core/home.html.accounts/identity_inventory.pyreport the enforced value (or drop the field) so theinventory cannot again claim closed-while-open.
accounts/tests/test_account_entrance_design.pyand related tests; add tests that GETshows the closed notice and POST creates no user.
Option B — keep local signup open, deliberately
Requires:
ACCOUNT_ALLOW_REGISTRATION = True(or delete the flag and make the inventory report theadapter's actual answer) so no dead, misleading setting remains.
_docs/specs/01-platform-architecture.md§ "Member signup, profile,Slack, and course registration" (the "when enabled" clause needs its condition defined).
ACCOUNT_EMAIL_VERIFICATION = "none"or move toverification; confirm the WAF signup rate rule promotion plan (spec 07).
/accounts/signup/as a first-class open page in the accessibility registry and corebrowser/smoke coverage (partially exists via
test_account_entrance_design.py).Option C — something else
E.g. invite-only signup, email-verified-only signup, or closing local signup while opening a
deliberate provider signup path. The owner should state the intended behavior in this issue;
the PM will re-groom into implementation criteria at that point.
Non-goals (all options)
(
accounts/auth.py).Dependencies
invitations, cohort onboarding flows).
Acceptance criteria
Apply to whichever option the owner picks. Nothing may be checked before the decision is
recorded in this issue by the owner.
this issue.
ACCOUNT_ALLOW_REGISTRATION(or itsdocumented replacement) is enforced by code allauth actually calls, or is removed.
accounts/identity_inventory.pyreports a value matching observed behavior, verified bya test.
GET /accounts/signup/renders the closed notice;POST /accounts/signup/creates no user and no
EmailAddressrow; the flag forced toTruein a test reopens theform (proving the gate is real, not hard-coded).
free" button (
templates/core/_site_shell_head.html) and the homepage CTAs(
templates/core/home.html) point at destinations consistent with it._docs/specs/01-platform-architecture.md§ "Member signup, profile, Slack, and courseregistration" states the decided signup posture.
/accounts/signup/(and the changedmasthead/homepage CTA under Option A) under the tester's screenshot gate.
Browser scenarios
consistent with the recorded decision, at desktop and mobile widths.
decided outcome, with errors announced accessibly wherever the form remains.
what a browser can actually do at
/accounts/signup/.