Wire case-level access control into evidence, sources, findings, leads routers #65

Closed
opened 2026-05-23 01:35:17 +00:00 by Mark · 4 comments
Owner

Parent Issue: #36 (Case-level access control) / #53

Scope

Wire require_case_access dependency into evidence, sources, findings, and leads routers.

Background

These four routers follow the same pattern: LIST by case_id query param, GET/PUT/DELETE by {id} path param. They are the core evidence trail within a case.

Acceptance Criteria

  • GET /evidence/?case_id=X — requires viewer case access; filters to only evidence in cases user can view
  • POST /evidence/ — requires editor case access (case_id in body)
  • GET /evidence/{id} — requires viewer case access (look up case_id from evidence item)
  • PUT /evidence/{id} — requires editor case access
  • DELETE /evidence/{id} — requires editor case access
  • Same pattern for sources, findings, leads routers
  • Admin bypass works for all endpoints
  • Existing tests updated

Dependencies

  • #64 (cases router + membership management endpoints must exist first)

Implementation Notes

  • For path-param endpoints (/{id}), need to look up the case_id from the entity before checking access
  • The require_case_access dep extracts case_id from path — may need a variant that resolves case_id from entity FK
  • For LIST endpoints, filter by cases where user has membership (join on CaseMembership)
## Parent Issue: #36 (Case-level access control) / #53 ## Scope Wire `require_case_access` dependency into **evidence, sources, findings, and leads routers**. ## Background These four routers follow the same pattern: LIST by `case_id` query param, GET/PUT/DELETE by `{id}` path param. They are the core evidence trail within a case. ## Acceptance Criteria - [ ] `GET /evidence/?case_id=X` — requires `viewer` case access; filters to only evidence in cases user can view - [ ] `POST /evidence/` — requires `editor` case access (case_id in body) - [ ] `GET /evidence/{id}` — requires `viewer` case access (look up case_id from evidence item) - [ ] `PUT /evidence/{id}` — requires `editor` case access - [ ] `DELETE /evidence/{id}` — requires `editor` case access - [ ] Same pattern for sources, findings, leads routers - [ ] Admin bypass works for all endpoints - [ ] Existing tests updated ## Dependencies - #64 (cases router + membership management endpoints must exist first) ## Implementation Notes - For path-param endpoints (`/{id}`), need to look up the `case_id` from the entity before checking access - The `require_case_access` dep extracts `case_id` from path — may need a variant that resolves case_id from entity FK - For LIST endpoints, filter by cases where user has membership (join on CaseMembership)
Author
Owner

⏸ Needs Decision

Blocking factors:

  • Didn't meet auto-approve criteria — scope=medium, risk=medium, spec=aligned

Scope: medium Risk: medium Spec: aligned Type: feature
Summary: Wire existing require_case_access dependency (from #64) into evidence, sources, findings, and leads routers to enforce case-level permissions — viewer for reads, editor for writes — with admin bypass.
Suggestion: Add a generic require_entity_access(model_cls, min_role) dep factory to deps.py that resolves case_id from the entity FK for /{id} endpoints. For LIST endpoints, join-filter on CaseMembership. Wire into all 4 routers. Update evidence review/lock endpoints too. Tests: add access-control test modules for each router.
Rationale: Medium scope: 4 routers x ~4 endpoints each = 16 endpoints, plus new dep factory in deps.py. The pattern already exists (require_case_access from #64, get_case_ownership in deps.py) so it's additive not novel. Medium risk: evidence router is 670 lines with complex review/lock logic — higher touch surface for regressions. Security-sensitive but the dep is well-tested and admin bypass is built into CaseAccessService. Fully aligned with parent issues #36/#53 and the existing architecture.

Action: Review each blocking factor above. Remove blocked:needs-decision label to route back to triage.

## ⏸ Needs Decision **Blocking factors:** - **Didn't meet auto-approve criteria** — scope=medium, risk=medium, spec=aligned --- **Scope:** medium **Risk:** medium **Spec:** aligned **Type:** feature **Summary:** Wire existing require_case_access dependency (from #64) into evidence, sources, findings, and leads routers to enforce case-level permissions — viewer for reads, editor for writes — with admin bypass. **Suggestion:** Add a generic require_entity_access(model_cls, min_role) dep factory to deps.py that resolves case_id from the entity FK for /{id} endpoints. For LIST endpoints, join-filter on CaseMembership. Wire into all 4 routers. Update evidence review/lock endpoints too. Tests: add access-control test modules for each router. **Rationale:** Medium scope: 4 routers x ~4 endpoints each = 16 endpoints, plus new dep factory in deps.py. The pattern already exists (require_case_access from #64, get_case_ownership in deps.py) so it's additive not novel. Medium risk: evidence router is 670 lines with complex review/lock logic — higher touch surface for regressions. Security-sensitive but the dep is well-tested and admin bypass is built into CaseAccessService. Fully aligned with parent issues #36/#53 and the existing architecture. **Action:** Review each blocking factor above. Remove `blocked:needs-decision` label to route back to triage.
Author
Owner

GSD VERIFY phase failed after 3 attempts.
═══ Gate: verify-passed (osint-hub#65) ═══
Mode: Tier 1 (functional only)

GATE OK: All gates up to 'implement' pass
── Tier 1: Functional ────────────────────────────────
Running tests...
❌ Tests failed (exit 4)
configfile: pyproject.toml
plugins: anyio-4.13.0, httpx-0.36.2, timeout-2.4.0, xdist-3.8.0, cov-7.1.0, split-0.11.0, asyncio-1.4.0
timeout: 60.0s
timeout method: signal
timeout func_only: False
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 0 items

============================ no tests ran in 0.00s =============================
ERROR: file or directory not found: tests/

============================= test session starts ==============================
platform linux -- Python 3.11.14, pytest-9.0.3, pluggy-1.6.0
rootdir: /home/b/Projects/osint-hub
configfile: pyproject.toml
plugins: anyio-4.13.0, httpx-0.36.2, timeout-2.4.0, xdist-3.8.0, cov-7.1.0, split-0.11.0, asyncio-1.4.0
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 0 items

============================ no tests ran in 0.00s =============================
Running linter...
✅ Lint clean
Running type checker...
✅ Types check clean
Previous test count: unknown

❌ GATE FAIL: verify-passed
- Tests failed with exit code 4
GATE FAIL: verify — blocked at 2026-05-27T00:13:51Z
Class: test-failure
Evidence: Verification failed: 1 failures
Git tag: osint-hub#65-gate-verify-fail-c1945eb21c7c (use gate-retry after fixing)

GSD VERIFY phase failed after 3 attempts. ═══ Gate: verify-passed (osint-hub#65) ═══ Mode: Tier 1 (functional only) GATE OK: All gates up to 'implement' pass ── Tier 1: Functional ──────────────────────────────── Running tests... ❌ Tests failed (exit 4) configfile: pyproject.toml plugins: anyio-4.13.0, httpx-0.36.2, timeout-2.4.0, xdist-3.8.0, cov-7.1.0, split-0.11.0, asyncio-1.4.0 timeout: 60.0s timeout method: signal timeout func_only: False asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 0 items ============================ no tests ran in 0.00s ============================= ERROR: file or directory not found: tests/ ============================= test session starts ============================== platform linux -- Python 3.11.14, pytest-9.0.3, pluggy-1.6.0 rootdir: /home/b/Projects/osint-hub configfile: pyproject.toml plugins: anyio-4.13.0, httpx-0.36.2, timeout-2.4.0, xdist-3.8.0, cov-7.1.0, split-0.11.0, asyncio-1.4.0 asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function collected 0 items ============================ no tests ran in 0.00s ============================= Running linter... ✅ Lint clean Running type checker... ✅ Types check clean Previous test count: unknown ❌ GATE FAIL: verify-passed - Tests failed with exit code 4 GATE FAIL: verify — blocked at 2026-05-27T00:13:51Z Class: test-failure Evidence: Verification failed: 1 failures Git tag: osint-hub#65-gate-verify-fail-c1945eb21c7c (use gate-retry after fixing)
Author
Owner

GSD VERIFY phase found no changes after implementation.
Worker output: ]11;?\API call failed after 3 retries: HTTP 429: Error code: 429 - {'status': 429, 'title': 'Too Many Requests'}

GSD VERIFY phase found no changes after implementation. Worker output: ]11;?\API call failed after 3 retries: HTTP 429: Error code: 429 - {'status': 429, 'title': 'Too Many Requests'}
Mark 2026-07-31 06:23:40 +00:00
Author
Owner

QA Verification (2026-08-04)

Independently re-verified by tasker-worker on commit 4fe3b33 (HEAD of main). The fix commit dddb058 is present and:

  • test_case_access.py: 82/82 pass (permission checks, admin bypass, role hierarchy, all four routers)
  • test_evidence_review.py: all tests pass (clean tree, fix in place)
  • test_case_membership.py: 11/13 pass; 2 pre-existing FK-constraint failures unrelated to #65

Code review of dddb058:

  • backend/app/deps.py adds require_case_access_by_entity(model_cls, min_role) factory that resolves case_id via session.get(model_cls, item_id) before delegating to CaseAccessService.can_access. Admin bypass wired at line 142.
  • backend/app/routers/evidence.py: GET /{item_id}, /{item_id}/detail, /{item_id}/artifact, PUT /{item_id}, DELETE /{item_id} all wired to require_case_access_by_entity(EvidenceItem, viewer|editor). LIST list_all() filters via CaseMembership join for non-admins when no case_id query is supplied.
  • backend/app/routers/{findings,leads,sources}.py: list endpoints use require_case_access_query_param from #64; path-param endpoints use the new factory.

The previously-pending GSD VERIFY comments (id 12813, 33270) failed for unrelated reasons (HTTP 429 / test infra issue). The fix itself is correct.

Issue state matches: closed 2026-08-01T00:43:24Z (kilo-gsd-runner card t_b22133bb). Closing out the QA reconciliation queue.

## QA Verification (2026-08-04) Independently re-verified by tasker-worker on commit 4fe3b33 (HEAD of main). The fix commit dddb058 is present and: - test_case_access.py: 82/82 pass (permission checks, admin bypass, role hierarchy, all four routers) - test_evidence_review.py: all tests pass (clean tree, fix in place) - test_case_membership.py: 11/13 pass; 2 pre-existing FK-constraint failures unrelated to #65 Code review of dddb058: - backend/app/deps.py adds require_case_access_by_entity(model_cls, min_role) factory that resolves case_id via session.get(model_cls, item_id) before delegating to CaseAccessService.can_access. Admin bypass wired at line 142. - backend/app/routers/evidence.py: GET /{item_id}, /{item_id}/detail, /{item_id}/artifact, PUT /{item_id}, DELETE /{item_id} all wired to require_case_access_by_entity(EvidenceItem, viewer|editor). LIST list_all() filters via CaseMembership join for non-admins when no case_id query is supplied. - backend/app/routers/{findings,leads,sources}.py: list endpoints use require_case_access_query_param from #64; path-param endpoints use the new factory. The previously-pending GSD VERIFY comments (id 12813, 33270) failed for unrelated reasons (HTTP 429 / test infra issue). The fix itself is correct. Issue state matches: closed 2026-08-01T00:43:24Z (kilo-gsd-runner card t_b22133bb). Closing out the QA reconciliation queue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
Mark/osint-hub#65
No description provided.