Skip to content

spring: Add reset-ticket admin escape hatch (3/4 split of #168) - #171

Open
RudraBJoshi wants to merge 3 commits into
Open-Coding-Society:masterfrom
dhyantsoni:split/spring/s3-reset-ticket-escape-hatch
Open

spring: Add reset-ticket admin escape hatch (3/4 split of #168)#171
RudraBJoshi wants to merge 3 commits into
Open-Coding-Society:masterfrom
dhyantsoni:split/spring/s3-reset-ticket-escape-hatch

Conversation

@RudraBJoshi

Copy link
Copy Markdown

Splitting #168 into smaller, independently-reviewable PRs across spring/flask/pages. This one covers ticketing for password reset attempts.

Independent of the other PRs in this stack — doesn't touch the OAuth reset endpoints or Flask sync, just adds a self-contained admin escape hatch.

Lets a user who hits the OAuth password-reset rate limit raise a ResetTicket instead of waiting out the window; an admin resolves it from the person/read portal, granting a batch of 5 extra reset attempts. Ticket creation is unauthenticated and rate-limited per caller IP (separate from the global rate limiter) so it can't be used to spam the admin queue. Also fixes a silent 500 on every real ticket-creation request (wrong JPA id-generation strategy for SQLite) and a security-config gap that made the ticket endpoint require login, defeating its purpose. Includes scripts/inject_reset_tickets.py for exercising the real endpoint in local testing.

Original PR: #168

RudraBJoshi and others added 3 commits August 24, 2026 11:22
Lets a user who hits the OAuth password-reset rate limit raise a
ResetTicket instead of waiting out the window; an admin resolves it from
the person/read portal, granting a batch of 5 extra reset attempts
(ResetCode.grantBonusAttempts).

Ticket creation is unauthenticated and takes an arbitrary uid, so its
per-uid idempotency check alone doesn't stop someone paging through many
different uids to spam the admin queue -- added a 5-requests-per-15-
minutes-per-IP limit (ResetCode.canRequestTicket), separate from the
global RateLimitFilter which is tuned for gross abuse, not this pattern.

Also fixes a silent 500 on every real ticket-creation request:
ResetTicket's GenerationType.AUTO resolves to sequence-table ID
generation on this SQLite dialect, and no such sequence table exists
under ddl-auto=none. Switched to GenerationType.IDENTITY, matching every
other SQLite-backed entity here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reset-ticket endpoint was never added to MvcSecurityConfig's permitAll
list, so anonymous requests fell through to anyRequest().authenticated()
and got redirected to /login (302) instead of creating a ticket. This
endpoint exists specifically for a rate-limited user who is, by definition,
not logged in -- every test of it this session used an authenticated admin
session (via curl with a saved cookie jar), which never exercised the
actual anonymous-caller path and masked the bug completely.

Found by writing scripts/inject_reset_tickets.py to call the endpoint the
way a real locked-out user would: no session. Its first run reported "200"
for a ticket that was never created, because Python's urllib followed the
302 to /login and reported that page's 200 instead.

/mvc/person/reset/ticket/{id}/grant (admin-only) is deliberately left off
permitAll -- it already falls through to anyRequest().authenticated() plus
the controller's own ROLE_ADMIN check, same pattern as the pre-existing
/mvc/person/reset/admin/{id}.

Verified anonymously with curl -i: 200 with no Location header, row
persisted in reset_ticket.
Calls the real POST /mvc/person/reset/ticket endpoint rather than inserting
rows via SQL directly -- exercises the actual idempotency check, the
per-IP rate limit (ResetCode.canRequestTicket), and doesn't risk drifting
from the schema the way hand-written SQL did earlier this session
(GenerationType.AUTO vs IDENTITY, see the "Ticket-creation rate limiting"
section of forgot-password-pipeline.md).

Uses a redirect-refusing opener rather than urllib's default: a 3xx here
means the endpoint started requiring auth again (exactly the bug fixed in
the previous commit) and should be reported as a failure, not silently
followed and reported as a false 200.

Usage: python3 scripts/inject_reset_tickets.py <uid> [<uid> ...] [--db-check]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant