Controller/routing cleanup: trailing-slash middleware, admin_only! helper, test settings isolation#238
Controller/routing cleanup: trailing-slash middleware, admin_only! helper, test settings isolation#238alexskr wants to merge 8 commits into
Conversation
Trailing-slash canonicalization was implemented in init.rb by registering
a companion "#{pattern}/" 301-redirect route for every route. That route
ran in the route-dispatch phase, so it sat behind every before filter and
doubled the route table, and it hardcoded 301 for all verbs (which can
downgrade the method and drop POST/PUT/PATCH bodies).
Replace it with Rack::TrailingSlashRedirect, used as the innermost
middleware so it runs before Sinatra routing and filters. It strips the
trailing slash and redirects (301 for GET/HEAD, 308 for other verbs to
preserve method and body), preserving the forwarded scheme
(X-Forwarded-Proto), the query string, and percent-encoded path segments.
7df10b1 to
a7d7172
Compare
The guard `error 403, "Access denied" unless current_user && current_user.admin?` was copy-pasted across slices (4x) and annotator (2x), with a nil-guard-less variant in users. Extract a single admin_only! helper next to current_user and call it from all seven sites.
a7d7172 to
0850b6b
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #238 +/- ##
===========================================
+ Coverage 78.10% 78.30% +0.20%
===========================================
Files 66 67 +1
Lines 3727 3748 +21
===========================================
+ Hits 2911 2935 +24
+ Misses 816 813 -3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
mdorf
left a comment
There was a problem hiding this comment.
This could use a bit of additional test coverage:
Not adequate for the new code as committed. The fix is cheap and the author has already written the assertions — commit them as test/middleware/test_trailing_slash_redirect.rb (the repo already has that convention via test_rack_attack.rb), as backend-free Rack::MockRequest unit tests covering:
POST/PUT/PATCH/DELETE /x/ → 308 with Location preserved
HEAD /x/ → 301 (and assert empty body, per the review finding)
root / and a no-trailing-slash path → pass through untouched
Optionally a 403 test on one annotator admin route. The verb-split test is the one I'd block on.
Data-driven test asserting every reachable admin_only!-guarded endpoint (slices create/update/delete, user delete, annotator dictionary/cache) returns 403 for an authenticated non-admin.
Adds test/middleware/test_trailing_slash_redirect.rb driven directly via Rack::MockRequest (no app boot / backend) covering: the verb split (301 for GET/HEAD, 308 for POST/PUT/PATCH/DELETE), HEAD redirect with an empty body, root and non-trailing-slash pass-through, and Location building (query string, percent-encoded segments, X-Forwarded-Proto scheme selection). Also fixes the middleware to return an empty body for HEAD redirects (a HEAD response must not carry a body).
|
Addressed in de5f8b3. Added
The optional non-admin |
The redirect Location is built from X-Forwarded-Proto, but an outer Rack::Cache keys entries on path + query only. Without no-store a 301/308 generated for an https client could be cached and replayed to a plain-http client (or vice versa), reintroducing the scheme-downgrade from issue #217. Redirects are cheap, so keeping them out of the shared cache costs nothing.
Settings like enable_security live on global singletons, so a suite that flips one (or errors after flipping it) leaked the value into later tests. Add a with_settings block helper that restores prior values even when the block raises, plus per-test (before_setup/after_teardown) and per-suite (before_all/after_all) snapshot/restore nets in the test base class. Suites can now set what they need and never restore. Drop the ad-hoc enable_security/disable_security/reset_security class helpers and convert callers.
after_all ran after_suite before restoring the per-suite settings snapshot. Suites that enable a setting in before_suite (e.g. test_rack_attack sets enable_security) and do privileged cleanup in after_suite (deleting users) then ran that teardown with the setting still active -> WriteAccessDeniedError. Because after_suite raised, restore_settings never ran, so enable_security leaked into every later suite (data-setup WriteAccessDenied, 401s on public routes). Restore first so teardown runs with settings rolled back and a raising after_suite cannot leak.
Summary
Controller/routing cleanup plus test-isolation hardening. No client-visible behavior change beyond trailing-slash redirects now using correct per-verb status codes and
Cache-Control: no-store.1. Move trailing-slash redirect to Rack middleware
Trailing-slash canonicalization was done in
init.rbby registering a companion"#{pattern}/"301-redirect route for every route. That approach:beforefilter and doubled the route table;301for all verbs, which can downgrade the method and drop POST/PUT/PATCH bodies.Replaced with
Rack::TrailingSlashRedirect(lib/rack/trailing_slash_redirect.rb), wired as the innermost middleware so it runs before Sinatra routing. It strips the trailing slash and redirects — 301 for GET/HEAD, 308 for other verbs (preserves method + body) — preserving forwarded scheme (X-Forwarded-Proto), query string, and percent-encoded path segments. HEAD redirects carry no body, and the redirect is markedCache-Control: no-store(its scheme-dependentLocationmust not be stored by the outerRack::Cacheand replayed to a different-scheme client).2. Extract admin_only! helper
The guard
error 403, "Access denied" unless current_user && current_user.admin?was copy-pasted acrossslices(4x) andannotator(2x), with a nil-guard-less variant inusers. Extracted a singleadmin_only!helper next tocurrent_user, called from all seven sites.3. Isolate process-wide settings in tests
Settings like
enable_securitylive on global singletons, so a suite that flips one (or errors after flipping it) leaked the value into later tests. Added awith_settingsblock helper (restores prior values even on raise) plus per-test and per-suite snapshot/restore nets in the test base class (test/test_case.rb), and converted the suites. Drops the ad-hocenable_security/disable_security/reset_securityhelpers.Tests
test/middleware/test_trailing_slash_redirect.rb— backend-freeRack::MockRequestunit tests: verb split (301 GET/HEAD, 308 others), HEAD → 301 with empty body, root + no-trailing-slash pass-through,Locationbuilding (query string,%2F-encoded segments,X-Forwarded-Proto), andCache-Control: no-store. Runs in ~3ms with no backend.test/controllers/test_admin_only_endpoints.rb— data-driven gate test asserting every reachableadmin_only!endpoint returns403for an authenticated non-admin.Notes
get_ontology_and_submission+check_last_modified_segmentin classes/instances via a namespacebeforefilter was dropped: sinatra-contrib scopes namespace filters by URL prefix, so it fired for sibling routes in other controllers under/classes/:cls/.init.rbredirect had the samehost_with_port/X-Forwarded-Protologic);no-storecloses the cache angle./slices/synchronize_groupsshadowed by/:slice_id).