Add keep flag to protect snapshots from retention cleanup #148

Manually merged
marvin8 merged 6 commits from feat/issue-144-keep-snapshots into main 2026-09-07 07:55:30 +00:00
Collaborator

Adds a keep boolean column to snapshots (migration V6) and a toggle endpoint (POST /snapshots/{id}/keep) so events or single frames can be protected from retention cleanup.

  • Keeping a motion frame protects its entire journey; keeping a background frame protects just that frame
  • Retention cleanup (delete_expired_snapshots) skips kept rows
  • Scrubber: Keep/Unkeep button beside the playback controls, keep state in the inline JSON, green kept markers on the scrub track
  • Day view: Keep/Unkeep button per event tile with a green kept badge
  • Removes the orphaned _keep_btn.html scaffolding left over from the defunct clips concept
  • Docs: web-ui.md "Keeping snapshots" section; Release-Notes entry

Closes #144

Adds a `keep` boolean column to snapshots (migration V6) and a toggle endpoint (`POST /snapshots/{id}/keep`) so events or single frames can be protected from retention cleanup. - Keeping a motion frame protects its entire journey; keeping a background frame protects just that frame - Retention cleanup (`delete_expired_snapshots`) skips kept rows - Scrubber: Keep/Unkeep button beside the playback controls, keep state in the inline JSON, green kept markers on the scrub track - Day view: Keep/Unkeep button per event tile with a green kept badge - Removes the orphaned `_keep_btn.html` scaffolding left over from the defunct clips concept - Docs: web-ui.md "Keeping snapshots" section; Release-Notes entry Closes #144
📝 Document the snapshot keep flag
Some checks failed
/ gitleaks (pull_request) Successful in 2m4s
/ checks (pull_request) Failing after 3m43s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 6m47s
72e031530c
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -436,1 +504,4 @@
)
@router.post("/snapshots/{snapshot_id}/keep")

security [MEDIUM]

New state-changing endpoint has no visible authentication or authorization check (no Depends(auth) or router-level auth in the diff). Any caller who can reach the web UI can toggle keep on arbitrary snapshot IDs (IDOR), potentially bypassing retention cleanup and exhausting storage. Add an authentication/authorization dependency and verify the caller owns or is permitted to modify the snapshot (A01).

**security** [MEDIUM] New state-changing endpoint has no visible authentication or authorization check (no Depends(auth) or router-level auth in the diff). Any caller who can reach the web UI can toggle keep on arbitrary snapshot IDs (IDOR), potentially bypassing retention cleanup and exhausting storage. Add an authentication/authorization dependency and verify the caller owns or is permitted to modify the snapshot (A01). <!-- wuming:sha256:004453f5aee37e87881014f7c68ffb9867bf6b79bb1effa70164c9b6f106bb5e -->
marvin8 marked this conversation as resolved
♻️ Extract scrubber row resolution to lower snapshot_album complexity
Some checks failed
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Failing after 1m23s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m22s
6603b06eb1
forgejo-actions left a comment

WuMing

Found 1 issue(s). See inline comments below.

## WuMing Found **1** issue(s). See inline comments below.
@ -436,1 +545,4 @@
)
@router.post("/snapshots/{snapshot_id}/keep")

security [MEDIUM]

A01: This state-changing endpoint has no authentication, authorization, or CSRF protection. Any client that can reach the web UI can toggle keep on arbitrary snapshot IDs, including every snapshot in the resolved journey, and may interfere with retention protection. Require an authenticated user and ownership/permission verification before mutating snapshots, and add CSRF protections for state-changing routes.

**security** [MEDIUM] A01: This state-changing endpoint has no authentication, authorization, or CSRF protection. Any client that can reach the web UI can toggle keep on arbitrary snapshot IDs, including every snapshot in the resolved journey, and may interfere with retention protection. Require an authenticated user and ownership/permission verification before mutating snapshots, and add CSRF protections for state-changing routes. <!-- wuming:sha256:39f51d0b3f9540bbf9bb4eed06e216579a8145384bdd85a84214e52b3444ba4a -->
marvin8 marked this conversation as resolved
🐛 Assert on kept badge markup, not the CSS class name
All checks were successful
/ gitleaks (pull_request) Successful in 14s
/ checks (pull_request) Successful in 1m33s
/ publish (pull_request) Has been skipped
/ deploy-docs (pull_request) Has been skipped
/ publish-container (pull_request) Has been skipped
/ pr-review (pull_request) Successful in 4m26s
ac556635ad
Author
Collaborator

src/cang/web/routes/snapshots.py

New state-changing endpoint has no visible authentication or authorization check (no Depends(auth) or router-level auth in the diff)…

🔴 Not actioned — false premise. The endpoint is authenticated: app.py mounts the snapshots router with app.include_router(snapshots_router, dependencies=[Depends(require_auth(cfg.web))]), which applies to every route in that router including this one. Unauthenticated calls raise NotAuthenticatedException and are redirected to /login. The check is intentionally router-level (pre-existing pattern shared by all routes), so it does not appear in this diff. Ownership checks do not apply — cang is a single-user NVR.

[`src/cang/web/routes/snapshots.py`](https://forge.marvin8.zone/marvin8/cang/pulls/148#issuecomment-2191) > New state-changing endpoint has no visible authentication or authorization check (no Depends(auth) or router-level auth in the diff)… 🔴 Not actioned — false premise. The endpoint is authenticated: `app.py` mounts the snapshots router with `app.include_router(snapshots_router, dependencies=[Depends(require_auth(cfg.web))])`, which applies to every route in that router including this one. Unauthenticated calls raise `NotAuthenticatedException` and are redirected to `/login`. The check is intentionally router-level (pre-existing pattern shared by all routes), so it does not appear in this diff. Ownership checks do not apply — cang is a single-user NVR.
Author
Collaborator

src/cang/web/routes/snapshots.py

A01: This state-changing endpoint has no authentication, authorization, or CSRF protection…

🔴 Not actioned — same finding as review 191 (see reply above): authentication is enforced at router level via Depends(require_auth(cfg.web)) in app.py. CSRF is mitigated by the session cookie being set with SameSite=Lax (auth.py), so cross-site POSTs do not carry the session; single-user NVR, no multi-user authorization model to enforce.

[`src/cang/web/routes/snapshots.py`](https://forge.marvin8.zone/marvin8/cang/pulls/148#issuecomment-2194) > A01: This state-changing endpoint has no authentication, authorization, or CSRF protection… 🔴 Not actioned — same finding as review 191 (see reply above): authentication is enforced at router level via `Depends(require_auth(cfg.web))` in `app.py`. CSRF is mitigated by the session cookie being set with `SameSite=Lax` (`auth.py`), so cross-site POSTs do not carry the session; single-user NVR, no multi-user authorization model to enforce.
marvin8 approved these changes 2026-09-07 07:51:10 +00:00
marvin8 manually merged commit 5a66cf9189 into main 2026-09-07 07:55:30 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marvin8/cang!148
No description provided.