Harden input validation and add security documentation #244
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "243-harden-input-validation-and-add-security"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Harden property_id validation across 4 controllers to prevent 500 errors from invalid foreign keys, add unauthenticated request specs for all protected controllers, and document the app's security posture.
Changes
app/controllers/application_controller.rb-- Add sharedfind_property_or_rejecthelper for property ID lookupapp/controllers/days_controller.rb-- Guardadd_to_queueagainst non-existent property_id (returns 422)app/controllers/work_queue_items_controller.rb-- Guardcreate(when no client_name) andmark_otheragainst non-existent property_id (returns 422)app/controllers/weeks_controller.rb-- Extendtoggle_assignguard to reject non-existent positive property IDs (returns 422)spec/requests/days_spec.rb-- Add invalid property_id specs and unauthenticated access specspec/requests/work_queue_items_spec.rb-- Add invalid property_id specs for create and mark_other, unauthenticated access specspec/requests/weeks_spec.rb-- Add invalid property_id spec for toggle_assign, unauthenticated access specspec/requests/properties_spec.rb-- Add unauthenticated access specspec/requests/uploads_spec.rb-- Add unauthenticated access specdocs/security.md-- New security posture document with auth model, role hierarchy, attack surface diagram, input validation patterns, CSRF, secrets management, and network securityTest Plan
docker compose run --rm -e RAILS_ENV=test web bundle exec rspec spec/requests/Review Checklist
Related
Closes #243
Related Notes
PR #244 Review
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, RSpec request specs, Hotwire/Turbo Stream, Markdown documentation.
Controller hardening (4 controllers, 1 shared helper)
The
find_property_or_rejecthelper inApplicationControlleris clean -- it does one thing (Property.find_by(id:)) and returns nil on miss, leaving rejection handling to each caller. This is the right call vs. a globalbefore_actionbecause only 4 specific actions need this guard, and each has different response formats (Turbo Stream vs. HTML redirect vs. barehead).All four guarded actions are the correct set. I verified via grep that no other controller accepts
property_idfrom params. ThePropertiesControllercorrectly usesProperty.find(params[:id])which raisesRecordNotFound(Rails 404) -- appropriate since the property IS the resource there, not a foreign key.Response format consistency:
DaysController#add_to_queue: Turbo Stream 422 + HTML redirect -- correct, matches the existing "Already in queue" pattern in the same action.WorkQueueItemsController#create: Turbo Stream 422 + HTML redirect -- correct, matches existing error pattern.WorkQueueItemsController#mark_other: Turbo Stream 422 + HTML redirect -- correct, same pattern.WeeksController#toggle_assign: Barehead(:unprocessable_entity)-- correct, this action already useshead(:bad_request)forproperty_id <= 0andhead(:created)/head(:no_content)for success. JSON-only endpoint, no flash needed.DRY observation: The Turbo Stream error response block is repeated 3 times across
DaysControllerandWorkQueueItemsController(lines in diff: days_controller +24..+29, work_queue_items +76..+81, +197..+202). All three render the sameturbo_stream.append("flash-messages", partial: "shared/flash", ...)pattern with a 422 status. This is an existing pattern in the codebase (the "Already in queue" error inDaysControlleruses it too), so it is consistent rather than novel duplication. Not a blocker -- extracting a sharedrender_flash_error(message, redirect_path)helper would be a future cleanup, not a gate for this PR.Test coverage:
property_idspecs cover all 4 guarded actions:DaysController#add_to_queue(Turbo Stream + HTML),WorkQueueItemsController#create(Turbo Stream),WorkQueueItemsController#mark_other(Turbo Stream),WeeksController#toggle_assign(JSON). Good.WorkQueueItemsController#createspec also verifies theclient_namebypass path still works (creates a new property instead of looking up by ID). This is a smart edge case test.Security document (docs/security.md, 247 lines):
docs/auth-strategy.mdanddocs/keycloak-setup.md.require_roledeclarations. I verifiedPropertiesController(lead, admin, super_admin, except: [:show, :new, :edit, :create, :update]),DaysController(admin, super_admin),WeeksController(admin, super_admin),WorkQueueItemsController(member, lead, admin, super_admin, except: :index). All accurate.BLOCKERS
None.
NITS
Method naming:
find_property_or_rejectdoes not actually reject -- it returns nil and leaves rejection to the caller. A name likefind_propertyorfind_property_by_idwould be more accurate. The current name implies the method handles the rejection itself (like Rails'find_or_create_by!pattern). Very minor -- the docstring clarifies intent.HTML format assertion on days invalid property_id test: The spec asserts
expect(response).to redirect_to(day_path(date: tuesday))but does not verify the flash alert message. The Turbo Stream path checks for "Property not found" in the body, but the HTML path only checks the redirect target. Consider addingfollow_redirect!and checking for the alert, or at minimum assertingresponse.statusis 302. Low risk since the controller code clearly setsalert: "Property not found.".Hardcoded test ID
999999: All invalid property_id specs use999999as the non-existent ID. This works but is brittle if the test database ever auto-increments that high (unlikely but theoretically possible). A more robust pattern would beProperty.maximum(:id).to_i + 1or0. Not worth changing -- just noting.Security doc: Keycloak H2 reference: The attack surface diagram shows
H2 Database (PVC)for Keycloak. If Keycloak was migrated to PostgreSQL (common for production), this should be updated. If it genuinely runs H2 in production, the doc is accurate but worth a comment noting the operational risk.SOP COMPLIANCE
docker compose run --rm -e RAILS_ENV=test web bundle exec rspec spec/requests/)Closes #243)PROCESS OBSERVATIONS
docs/security.mdfile should be linked from the mainCLAUDE.mddocs table. Currently the table of contents inCLAUDE.mddoes not reference it. This can be a follow-up.VERDICT: APPROVED
PR #244 Review
Title: Harden input validation and add security documentation
Parent Issue: #243
Scope: 10 files changed, +381 / -3
DOMAIN REVIEW
Tech stack: Ruby on Rails 8.1, Hotwire/Turbo Streams, RSpec request specs, Mermaid diagrams in documentation.
Controller Changes (Input Validation)
The shared
find_property_or_rejecthelper inApplicationControlleris well-designed:find_by(returns nil) rather thanfind(raises), which is the correct pattern for validating a foreign key vs. finding the primary resource.before_action, avoiding unintended side effects on controllers that do not need it.find_property_or_rejectdoes not reject; it only looks up the property. The rejection logic lives in each calling controller. A more accurate name would befind_propertyorfind_property_by_id. This is a nit, not a blocker.All 4 identified user-input paths for
property_idare now guarded:DaysController#add_to_queue-- returns 422 with flash (Turbo Stream) or redirect (HTML)WorkQueueItemsController#create(non-client_name path) -- returns 422 with flash or redirectWorkQueueItemsController#mark_other-- returns 422 with flash or redirectWeeksController#toggle_assign-- returns 422 viahead(:unprocessable_entity)The error handling in
WeeksController#toggle_assignis correctly simpler (justhead :unprocessable_entity) because that endpoint is JSON-only. The three Turbo Stream endpoints all render flash messages, which is consistent with the existing error patterns in those controllers.DRY Observation (non-blocking)
The respond_to block for invalid property_id is repeated three times across
DaysController#add_to_queue,WorkQueueItemsController#create, andWorkQueueItemsController#mark_other. The pattern is identical each time:This is the same pattern already used for other error responses in these controllers (e.g., duplicate queue item, save failure). Extracting it into a shared
render_validation_error(message, redirect_path:)helper would reduce repetition, but since this pattern is already established across the codebase (not introduced by this PR), this is not a blocker. It is a future improvement opportunity.Test Coverage
Strong coverage. The PR adds:
property_idspecs for all 4 guarded actions (Turbo Stream and HTML formats tested for days and work_queue_items; JSON-only for weeks)does not reject when client_name is provided) that confirms thecreatepath still works when creating a property inline -- this is an important regression guardOne observation: the
days_spec.rbinvalid property_id test for HTML format assertsredirect_to(day_path(date: tuesday))but does not verify the flash alert message. This is minor since the Turbo Stream test already validates the message text.Security Documentation
The
docs/security.mdis thorough and reflects the actual posture rather than aspirational goals. Specific strengths:PropertiesControllerdoes NOT need thefind_property_or_rejectguard (it usesProperty.findfor the primary resource, which raises RecordNotFound / 404)Minor doc nit: The "Controller Role Map" references
DaysControlleras requiring "admin, super_admin" roles, which matches therequire_role :admin, :super_adminon line 2 ofdays_controller.rb. Verified correct.BLOCKERS
None identified.
property_idinput paths are now guarded..envfiles in the diff.find_property_or_rejecthelper is centralized; the respond_to blocks are duplicated but follow the existing pattern and are not auth logic.NITS
find_property_or_rejectnaming: The method only finds, it does not reject. Considerfind_propertyfor accuracy. The "or_reject" suffix implies the method itself handles the rejection, but callers must do that. Low priority.HTML format test for days invalid property_id: The test asserts redirect but does not verify the
alert: "Property not found."flash. Consider addingfollow_redirect!and checking for the message text, or assertingexpect(flash[:alert]).to eq("Property not found.").Magic number 999999: Used in all invalid property_id specs. Consider extracting to a shared
let(:nonexistent_id) { 999999 }or using a comment. Very minor.Security doc line length: Some lines in the Mermaid diagrams are long. Not a functional issue but may render oddly in narrow viewports.
SOP COMPLIANCE
.envfiles, or credentials committedPROCESS OBSERVATIONS
find_bycall adds one SELECT query per guarded action, but only when the action is invoked -- no impact on index/show performance.docs/security.mdfile is a meaningful operational artifact. It should be added to the docs table of contents inCLAUDE.md.VERDICT: APPROVED