Wire case-level access control into orgs, people, contacts, relationships routers #66

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

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

Scope

Wire require_case_access dependency into organizations, people, contacts, and relationships routers.

Background

These routers manage the entity graph within a case. Organizations and people are case-scoped. Contacts link orgs/people within a case. Relationships are case-scoped edges.

Acceptance Criteria

  • GET /organizations/?case_id=X — requires viewer case access
  • POST /organizations/ — requires editor case access (case_id in body)
  • GET /organizations/{id} — requires viewer case access (resolve case_id from org)
  • PUT /organizations/{id} — requires editor case access
  • DELETE /organizations/{id} — requires editor case access
  • Same pattern for people, contacts, relationships routers
  • Admin bypass works for all endpoints
  • Existing tests updated

Dependencies

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

Implementation Notes

  • Contacts router filters by org_id/person_id/case_id — need to resolve case_id from whichever ID is provided
  • Relationships are case-scoped via case_id FK — same pattern as evidence
## Parent Issue: #36 (Case-level access control) / #53 ## Scope Wire `require_case_access` dependency into **organizations, people, contacts, and relationships routers**. ## Background These routers manage the entity graph within a case. Organizations and people are case-scoped. Contacts link orgs/people within a case. Relationships are case-scoped edges. ## Acceptance Criteria - [ ] `GET /organizations/?case_id=X` — requires `viewer` case access - [ ] `POST /organizations/` — requires `editor` case access (case_id in body) - [ ] `GET /organizations/{id}` — requires `viewer` case access (resolve case_id from org) - [ ] `PUT /organizations/{id}` — requires `editor` case access - [ ] `DELETE /organizations/{id}` — requires `editor` case access - [ ] Same pattern for people, contacts, relationships routers - [ ] Admin bypass works for all endpoints - [ ] Existing tests updated ## Dependencies - #64 (cases router + membership management endpoints must exist first) ## Implementation Notes - Contacts router filters by `org_id`/`person_id`/`case_id` — need to resolve case_id from whichever ID is provided - Relationships are case-scoped via `case_id` FK — same pattern as evidence
Author
Owner

⏸ Needs Decision

Blocking factors:

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

Scope: small Risk: medium Spec: aligned Type: enhancement
Summary: Wire the existing require_case_access dependency into orgs, people, contacts, and relationships routers so endpoints enforce case-level membership checks on top of global role checks.
Suggestion: Add a per-router helper (e.g. _resolve_case_access) that fetches the entity, extracts its case_id, and calls require_case_access inline — since the require_case_access dep expects case_id as a path param, the GET-by-ID/PUT/DELETE endpoints need custom resolution. LIST endpoints with a case_id query param can use require_case_access directly via Depends. Contacts router needs case_id resolved from whichever filter param (org_id/person_id/case_id) is provided.
Rationale: Scope is small: 4 existing router files (<100 lines each), no new models or services needed — require_case_access dep, CaseAccessService, and CaseMembership model already exist. Risk is medium because the require_case_access dep signature expects a case_id path param, which doesn't exist on GET-by-ID/PUT/DELETE endpoints — those must resolve case_id from the entity itself (e.g. Organization.case_id), requiring either a custom dependency or inline helpers. The contacts router is the most complex since it uses a generic resource-based path scheme over 5 models. Admin bypass (user.role=='admin') is already baked into CaseAccessService.can_access() so no extra work there. Spec is aligned: issue is explicitly part of the case-level access control milestone (#36) and the infrastructure it depends on (#64 — cases router + membership mgmt) already exists. Existing tests for the service layer at backend/tests/unit/test_case_access.py are solid but no router-level or integration tests exist yet for these endpoints — those will need to be updated.

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=small, risk=medium, spec=aligned --- **Scope:** small **Risk:** medium **Spec:** aligned **Type:** enhancement **Summary:** Wire the existing require_case_access dependency into orgs, people, contacts, and relationships routers so endpoints enforce case-level membership checks on top of global role checks. **Suggestion:** Add a per-router helper (e.g. _resolve_case_access) that fetches the entity, extracts its case_id, and calls require_case_access inline — since the require_case_access dep expects case_id as a path param, the GET-by-ID/PUT/DELETE endpoints need custom resolution. LIST endpoints with a case_id query param can use require_case_access directly via Depends. Contacts router needs case_id resolved from whichever filter param (org_id/person_id/case_id) is provided. **Rationale:** Scope is small: 4 existing router files (<100 lines each), no new models or services needed — require_case_access dep, CaseAccessService, and CaseMembership model already exist. Risk is medium because the require_case_access dep signature expects a case_id path param, which doesn't exist on GET-by-ID/PUT/DELETE endpoints — those must resolve case_id from the entity itself (e.g. Organization.case_id), requiring either a custom dependency or inline helpers. The contacts router is the most complex since it uses a generic resource-based path scheme over 5 models. Admin bypass (user.role=='admin') is already baked into CaseAccessService.can_access() so no extra work there. Spec is aligned: issue is explicitly part of the case-level access control milestone (#36) and the infrastructure it depends on (#64 — cases router + membership mgmt) already exists. Existing tests for the service layer at backend/tests/unit/test_case_access.py are solid but no router-level or integration tests exist yet for these endpoints — those will need to be updated. **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#66) ═══
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-26T23:42:25Z
Class: test-failure
Evidence: Verification failed: 1 failures
Git tag: osint-hub#66-gate-verify-fail-ce2d42b6a8a0 (use gate-retry after fixing)

GSD VERIFY phase failed after 3 attempts. ═══ Gate: verify-passed (osint-hub#66) ═══ 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-26T23:42:25Z Class: test-failure Evidence: Verification failed: 1 failures Git tag: osint-hub#66-gate-verify-fail-ce2d42b6a8a0 (use gate-retry after fixing)
Mark 2026-06-01 17:51:07 +00:00
  • closed this issue
  • removed the
    quickfix
    label
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#66
No description provided.