Skip to content

feat: geofilter customizer tab + PUT /api/config/geo-filter#736

Open
efiten wants to merge 3 commits intoKpa-clawbot:masterfrom
efiten:feat/geofilter-m3-customizer
Open

feat: geofilter customizer tab + PUT /api/config/geo-filter#736
efiten wants to merge 3 commits intoKpa-clawbot:masterfrom
efiten:feat/geofilter-m3-customizer

Conversation

@efiten
Copy link
Copy Markdown
Contributor

@efiten efiten commented Apr 14, 2026

Summary

Part of #669 — M3: Customizer integration.

Backend

  • PUT /api/config/geo-filter (requires X-API-Key) — saves geo_filter to config.json atomically (temp+rename) and updates in-memory config immediately. No restart needed.
  • SaveGeoFilter() in config.go reads config as a raw map (preserving _comment fields and any unknown keys), updates just the geo_filter key, writes back.
  • GET /api/config/geo-filter now includes writeEnabled: booltrue when server has a strong apiKey configured.
  • Server.configDir field wired from -config-dir flag.
  • 7 new tests (TestPutConfigGeoFilter × 4, TestSaveGeoFilter × 3, updated TestConfigGeoFilterEndpoint × 2).

Frontend

  • New 🗺️ GeoFilter tab in the customizer between Display and Export.
  • Reads GET /api/config/geo-filter on tab open; shows current polygon on a Leaflet map.
  • Editing controls are hidden on public deployments (writeEnabled=false). On servers with a write-capable apiKey, the edit UI reveals itself: undo/clear, buffer km, API key input, Save and Remove buttons.
  • Save calls PUT /api/config/geo-filter; applies immediately in-memory.
  • Leaflet map instance is cleaned up on tab switch and panel close.

Docs

  • New docs/user-guide/geofilter.md: complete guide covering config schema, customizer tab, builder, prune script, and API.
  • Updated docs/user-guide/configuration.md and customization.md.
  • Updated config.example.json _comment to mention the Customizer tab.

Note: Editing controls are gated on writeEnabled (server-side flag) — non-admin users see a read-only polygon view. Admins enter their API key per-save; it is never stored client-side.

Depends on: #734 (M1) and #735 (M2) for context, but can be merged independently.

Test plan

  • Server with no apiKey: GeoFilter tab shows map, no edit controls
  • Server with apiKey: edit controls appear after tab loads
  • Draw polygon → Save with correct API key → polygon saved to config.json, reloads correctly
  • Clear filter → Remove filter → config.json no longer has geo_filter
  • Wrong API key → error message shown
  • Tab switch and panel close do not leave orphaned Leaflet maps
  • go test ./... passes

🤖 Generated with Claude Code

Copy link
Copy Markdown
Owner

@Kpa-clawbot Kpa-clawbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security Review — PUT /api/config/geo-filter

Reviewed with a security-focused lens on the config write endpoint. The overall design is solid — API key gating, atomic file writes, input shape validation, clear separation of read-only vs write-enabled UI. Good work. But there are a few issues that need addressing before merge.

Must-fix

1. Data race on s.cfg.GeoFilter (routes.go)
The PUT handler writes s.cfg.GeoFilter = gf with no synchronization. The GET handler and any ingest-time filtering read s.cfg.GeoFilter concurrently from other goroutines. This is a textbook data race — Go's race detector will flag it. Wrap config mutations in a mutex (or use atomic.Pointer if you want lock-free reads).

2. No request body size limit (routes.go:~L440)
json.NewDecoder(r.Body).Decode(&body) reads unbounded input. An attacker with a valid API key (or a compromised key) can send a multi-GB JSON body and exhaust server memory. Wrap with http.MaxBytesReader:

r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB

3. No coordinate range validation (routes.go)
Polygon points are stored as-is — no check that lat ∈ [-90, 90] and lon ∈ [-180, 180]. NaN, Infinity, or wildly out-of-range values would be persisted to config.json and could cause downstream issues (point-in-polygon math, Leaflet rendering). Validate each point after decoding.

Should-fix

4. writeEnabled leaks API key presence to unauthenticated callers (routes.go:~L429)
The GET endpoint (no auth required) returns writeEnabled: true/false, which tells any anonymous visitor whether the server has a strong API key configured. This is a minor information leak — consider removing it from the unauthenticated response and having the UI infer editability from a separate authenticated probe, or accept the risk with a comment explaining the trade-off.

5. No upper bound on polygon point count
A polygon with 100,000 points is technically valid per the current checks. Consider a reasonable cap (e.g., 1000 points) to bound storage and computation.

Nits (non-blocking)

  • SaveGeoFilter error messages include filesystem paths (config.json not found in /app/data). These get logged server-side (fine) but also returned to the client via writeError. Consider generic client-facing messages.
  • The _comment field in config.example.json is a nice touch for discoverability.

What's done well

  • requireAPIKey middleware on the PUT — correct layer for authz
  • IsWeakAPIKey check for writeEnabled — defense in depth
  • Atomic write via temp + rename — prevents partial writes on crash
  • Polygon minimum 3 points validation
  • Comprehensive test coverage: auth rejection, save/clear/validation, disk persistence
  • UI correctly gates edit controls on server-reported writeEnabled
  • Clean separation: standalone builder for offline use, customizer tab for live editing

Items 1–3 are must-fix before merge. The rest can be addressed in follow-ups.

efiten added a commit to efiten/meshcore-analyzer that referenced this pull request Apr 19, 2026
- Fix data race on s.cfg.GeoFilter: add cfgMu RWMutex with getGeoFilter/
  setGeoFilter accessors used in all handler goroutines
- Add 1 MB MaxBytesReader cap on PUT /api/config/geo-filter request body
- Validate polygon coordinate ranges (lat ∈ [-90,90], lon ∈ [-180,180])
  and reject NaN/Inf values
- Cap polygon at 1000 points to bound storage and computation
- Document writeEnabled information-leak trade-off with a comment
- Add tests for coordinate range rejection and oversized polygon rejection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten
Copy link
Copy Markdown
Contributor Author

efiten commented Apr 20, 2026

All must-fix and should-fix items from the security review have been addressed in commit aae2e13 (plus a build fix in e12dc6d):

Must-fix — resolved:

  1. Data race — added cfgMu sync.RWMutex to Server, reads go through getGeoFilter() and writes through setGeoFilter().
  2. Body size limithttp.MaxBytesReader(w, r.Body, 1<<20) added at the top of the PUT handler.
  3. Coordinate validation — each polygon point is validated: lat ∈ [-90, 90], lon ∈ [-180, 180], rejects NaN and Infinity.

Should-fix — resolved:
4. writeEnabled info leak — accepted risk, added an inline comment in the GET handler explaining the trade-off (low-sensitivity info, public read-only clients need it).
5. Polygon point cap — hard limit of 1000 points enforced, returns 400 if exceeded.

Nit (filesystem path in error response) — left as-is; the path leaks only to authenticated callers (PUT requires X-API-Key) and matches the existing pattern in the codebase.

Build was also broken by a missing closing } on SaveGeoFilter — fixed in e12dc6d.

@efiten efiten requested a review from Kpa-clawbot April 20, 2026 07:43
@Kpa-clawbot
Copy link
Copy Markdown
Owner

Final Re-Review — NEEDS REBASE

Follow-up to the security review from 2026-04-15. Verifying @efiten's claimed fixes in aae2e13.

🔒 djb — Security Verification

Must-fix #1 (data race): ✅ Fixed. cfgMu sync.RWMutex added to Server, with getGeoFilter() (RLock) and setGeoFilter() (Lock). Both the GET handler (L449) and handleNodes (L1120) now use getGeoFilter(). PUT handler uses setGeoFilter().

Must-fix #2 (unbounded body): ✅ Fixed. http.MaxBytesReader(w, r.Body, 1<<20) at L471.

Must-fix #3 (coordinate validation): ✅ Fixed. NaN/Inf/range checks at L487-490, lat ∈ [-90,90], lon ∈ [-180,180].

Should-fix #4 (writeEnabled leak): ✅ Acknowledged. Comment at L448-450 documents the risk-accepted trade-off. Reasonable.

Should-fix #5 (polygon cap): ✅ Fixed. 1000-point cap at L484.

Nit (error paths leak): ✅ Fixed. SaveGeoFilter errors are logged server-side (log.Printf at L503) but the client gets generic "failed to save config" (L504).

All five original findings verified as resolved.

📐 dijkstra — Correctness of the Mutex Discipline

The RWMutex usage is correct: reads take RLock, writes take Lock. The invariant "in-memory ≥ disk" holds because SaveGeoFilter writes disk before setGeoFilter updates memory (L498-505). If the process crashes between disk write and memory update, the next startup loads from disk — convergent. If disk write fails, memory is unchanged — consistent.

One observation: s.cfg.APIKey is read outside the mutex (L451), but it's set once at startup and never mutated — no race.

🆕 New Findings

  1. SaveGeoFilter raw-map injection: Not exploitable. The PUT handler decodes into a strict struct{Polygon, BufferKm} — only those two typed fields reach SaveGeoFilter. The raw-map preserves other config keys but can't be poisoned via the API.

  2. UI read-only degradation: Clean. #cv2-gf-edit starts display:none, only shown when writeEnabled=true (L1421). Modal title changes to "read only" (L1215). No edit affordances leak.

  3. Test coverage: Good breadth — auth rejection, save/clear/validation, disk persistence, coordinate range, polygon cap (L2841-4033). However, no concurrent test exercises the race fix (e.g., parallel PUT + GET). Structural fix is sound, but a TestGeoFilterConcurrent with -race would be a nice addition in a follow-up.

Verdict

Ready after rebase. All must-fix and should-fix items verified as resolved. No new blocking issues. The PR is currently CONFLICTING against master — @efiten please rebase onto current master and force-push. After that, this is merge-ready.

@Kpa-clawbot
Copy link
Copy Markdown
Owner

@efiten can you please resurrect this PR - either rebase it and let's get it merged or if it's stale let's close it as such.

efiten and others added 3 commits May 1, 2026 13:36
…bot#669 M3)

Backend:
- Add PUT /api/config/geo-filter (requires X-API-Key) — saves geo_filter
  back to config.json atomically and updates in-memory config immediately,
  no restart needed
- Add SaveGeoFilter() to config.go: reads config as raw map (preserving
  _comment fields), updates geo_filter key, writes back via temp+rename
- Add writeEnabled field to GET /api/config/geo-filter response so the
  frontend can gate editing controls on server write capability
- Add Server.configDir field; wired from -config-dir flag in main.go
- Tests: TestPutConfigGeoFilter (4 cases) + TestSaveGeoFilter (3 cases)

Frontend:
- Add GeoFilter tab (🗺️) to the customizer between Display and Export
- Tab shows current polygon on a Leaflet map (read-only for all users)
- Editing controls (undo, clear, buffer km, API key input, save/remove)
  are only revealed when the server reports writeEnabled=true — i.e. the
  deployment has a write-capable apiKey configured. Public instances see
  a read-only polygon view.
- Save calls PUT /api/config/geo-filter; Remove clears the filter
- Map is destroyed on tab switch and panel close to avoid Leaflet leaks

Docs:
- Add docs/user-guide/geofilter.md (full guide: config, customizer,
  builder, prune script, API)
- Update configuration.md and customization.md with geo_filter section
- Update config.example.json _comment to mention the Customizer tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clicking the small inline map in the customizer GeoFilter tab now opens
a full-screen modal (92vw × 86vh) with Undo/Clear/Done/Cancel controls.
The inline map becomes a read-only preview. Both maps and the standalone
geofilter-builder.html now use CartoDB Positron (light) instead of dark.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix data race on s.cfg.GeoFilter: add cfgMu RWMutex with getGeoFilter/
  setGeoFilter accessors used in all handler goroutines
- Add 1 MB MaxBytesReader cap on PUT /api/config/geo-filter request body
- Validate polygon coordinate ranges (lat ∈ [-90,90], lon ∈ [-180,180])
  and reject NaN/Inf values
- Cap polygon at 1000 points to bound storage and computation
- Document writeEnabled information-leak trade-off with a comment
- Add tests for coordinate range rejection and oversized polygon rejection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@efiten efiten force-pushed the feat/geofilter-m3-customizer branch from e12dc6d to 5719b9e Compare May 1, 2026 12:11
@KpaBap KpaBap enabled auto-merge (squash) May 2, 2026 18:18
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.

2 participants