A shop filter that did not filter handed back the whole tenant

GET /api/cameras read only site_id, while every other filtered endpoint
takes both spellings through siteParam. So ?site=chennai was not a
filter at all but an unknown query parameter, silently ignored, and the
caller got every camera in the tenant believing it had one shop's.

Found by using it: a setup script saw another shop's cameras, concluded
three shops already had theirs and created none; then a delete aimed at
a test shop removed the live Coimbatore entrance camera, which had to be
restored. This is exactly the hazard already recorded for site vs
site_id - the note existed, the handler was simply missed.

One line to fix, and a test that asserts the whole class rather than
this one route: both spellings must narrow, and only an absent filter
may return more than one shop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
This commit is contained in:
2026-09-24 13:28:02 +05:30
parent 177584e812
commit 9062d2fc51
2 changed files with 68 additions and 1 deletions

View File

@@ -21,7 +21,13 @@ const snapshotTTL = 5 * time.Minute
func (s *Server) handleCameras(w http.ResponseWriter, r *http.Request) {
p := PrincipalFrom(r.Context())
siteID := trim(r.URL.Query().Get("site_id"))
// siteParam, not Query().Get("site_id"): every other filtered endpoint
// takes both spellings, and this one took only the longer. `?site=chennai`
// was therefore not a filter but an unknown parameter, silently ignored -
// so a caller asking for one shop's cameras was handed the whole tenant's.
// Measured: it made a script skip creating cameras for three shops because
// another shop's already existed, and deleted a camera from the wrong shop.
siteID := siteParam(r)
if siteID != "" {
var ok bool
if siteID, ok = s.resolveSiteFilter(w, r, siteID); !ok {