-
-
Notifications
You must be signed in to change notification settings - Fork 39
fix-forward #2976 (tsk-ob2mpd): ownership tests still assert the 403 existence-oracle the card removed #2992
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
168aa90
c16c233
de042b8
e83fe76
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ```markdown | ||
| ### Fixed | ||
|
|
||
| - Renamed `test_non_owner_update_returns_403`, `test_non_owner_delete_returns_403`, and `test_non_owner_archive_returns_403` to use `404` instead of `403` for non-owner mutation attempts, per project ownership design. This closes the existence oracle where "exists but forbidden" is indistinguishable from "does not exist" (from tsk-ob2mpd). | ||
|
|
||
| - Added `test_non_owner_oracle_closed` to verify the oracle is actually closed: both missing and forbidden project IDs return identical 404 response bodies. | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ### Fixed | ||
|
|
||
| - Projects router: Fixed half-finished store->pstore rename in six write handlers (update_project, archive_project, delete_project, add_member, set_project_lead, remove_member) that raised NameError at request time | ||
| - Projects events: Fixed ProjectEventBroker deadlock by releasing the lock before putting to subscriber queues and evicting oldest items on full queues instead of blocking | ||
| - Projects events: Preserved replay history on last-unsubscribe so reconnecting SSE clients can catch up on missed events |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| ### Fixed | ||
|
|
||
| - Projects router: Changed `require_owner_or_admin` to `_get_owned_project` for 6 routes to provide consistent 404 behavior for non-owners | ||
| - Projects router: Updated `delete_element` mode parameter to use `Literal["strict", "untag"]` for type safety | ||
| - Projects router: Consolidated `_SLUG_RE` regex definition from 3 locations to 1 in `element_store.py` | ||
| - Projects router: Added `_TaskRequestModelMixin` to `CreateChecklistItemIn` model | ||
| - Projects router: Fixed `project_events` stream to include `id` field in emitted events | ||
| - Projects events: Added `maxsize` parameter to prevent unbounded queue growth | ||
| - Projects events: Clean up empty subscriber keys to prevent memory leaks | ||
| - Element store: Updated import to use centralized `_SLUG_RE` from `element_store.py` | ||
| - Fixed imports in projects.py: removed unused `re` import, added `Literal` and `_SLUG_RE` imports |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| ### Added | ||
|
|
||
| - Added `assemble_bundle.sh` release-build smoke test verifying Sparkle.framework is bundled on success and missing-framework fails non-zero | ||
| - Added domain audit test ensuring no `taos.app` feed or download references remain under `mac/` | ||
|
|
||
| S2-23: Mac updater is a no-op: Sparkle never fetched; feed host is not the project domain |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -291,36 +291,42 @@ async def test_owner_can_get_own_project(member_client): | |
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_non_owner_update_returns_403(two_member_clients): | ||
| """A non-owner patching another user's project gets 403.""" | ||
| async def test_non_owner_update_returns_404(two_member_clients): | ||
| """WHY: a non-owner must not be able to distinguish 'exists but forbidden' from 'does not exist'.""" | ||
| alice, bob = two_member_clients | ||
| resp = await alice.post("/api/projects", json={"name": "A", "slug": "a-upd"}) | ||
| pid = resp.json()["id"] | ||
|
|
||
| resp = await bob.patch(f"/api/projects/{pid}", json={"name": "Hijacked"}) | ||
| assert resp.status_code == 403 | ||
| assert resp.status_code == 404 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_non_owner_delete_returns_403(two_member_clients): | ||
| """A non-owner deleting another user's project gets 403.""" | ||
| async def test_non_owner_delete_returns_404(two_member_clients): | ||
| """WHY: a non-owner must not be able to distinguish 'exists but forbidden' from 'does not exist'.""" | ||
| alice, bob = two_member_clients | ||
| resp = await alice.post("/api/projects", json={"name": "A", "slug": "a-del"}) | ||
| pid = resp.json()["id"] | ||
|
|
||
| resp = await bob.delete(f"/api/projects/{pid}") | ||
| assert resp.status_code == 403 | ||
| assert resp.status_code == 404 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_non_owner_archive_returns_403(two_member_clients): | ||
| """A non-owner archiving another user's project gets 403.""" | ||
| async def test_non_owner_oracle_closed(two_member_clients): | ||
| """Both missing and forbidden projects return identical 404 bodies.""" | ||
| alice, bob = two_member_clients | ||
| resp = await alice.post("/api/projects", json={"name": "A", "slug": "a-arch"}) | ||
| pid = resp.json()["id"] | ||
| resp = await alice.post("/api/projects", json={"name": "A", "slug": "alice-real"}) | ||
| alice_pid = resp.json()["id"] | ||
| non_existent_id = "proj-non-existent" | ||
|
|
||
| # Both return 404 with identical error messages | ||
| missing_resp = await bob.patch(f"/api/projects/{non_existent_id}", json={"name": "Hijacked"}) | ||
| forbidden_resp = await bob.patch(f"/api/projects/{alice_pid}", json={"name": "Hijacked"}) | ||
|
|
||
| resp = await bob.post(f"/api/projects/{pid}/archive") | ||
| assert resp.status_code == 403 | ||
| assert missing_resp.status_code == 404 | ||
| assert forbidden_resp.status_code == 404 | ||
| assert missing_resp.json() == forbidden_resp.json() | ||
|
Comment on lines
+316
to
+329
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add non-owner archive coverage and correct the changelog.
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ class ProjectEventBroker: | |
| """In-memory pub/sub. One channel per project_id. | ||
|
|
||
| Single-worker assumption: all subscribers and publishers share one process. | ||
| See spec §4 — multi-worker is out of scope. | ||
| See spec s4 -- multi-worker is out of scope. | ||
| """ | ||
|
|
||
| def __init__(self, replay_size: int = 32) -> None: | ||
|
|
@@ -27,22 +27,45 @@ def __init__(self, replay_size: int = 32) -> None: | |
| self._lock = asyncio.Lock() | ||
|
|
||
| async def subscribe(self, project_id: str) -> asyncio.Queue[ProjectEvent]: | ||
| queue: asyncio.Queue[ProjectEvent] = asyncio.Queue() | ||
| queue: asyncio.Queue[ProjectEvent] = asyncio.Queue(maxsize=self._replay_size) | ||
| async with self._lock: | ||
| self._queues.setdefault(project_id, []).append(queue) | ||
| for ev in self._replay.get(project_id, ()): | ||
| queue.put_nowait(ev) | ||
| try: | ||
| queue.put_nowait(ev) | ||
| except asyncio.QueueFull: | ||
| break | ||
| return queue | ||
|
|
||
| async def unsubscribe(self, project_id: str, queue: asyncio.Queue[ProjectEvent]) -> None: | ||
| async with self._lock: | ||
| qs = self._queues.get(project_id, []) | ||
| if queue in qs: | ||
| qs.remove(queue) | ||
| if not qs: | ||
| self._queues.pop(project_id, None) | ||
| # Keep _replay so a reconnecting subscriber (e.g. a reopened | ||
| # SSE connection) can catch up on events it missed. The deque | ||
| # has a fixed maxlen so memory stays bounded. | ||
|
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Bound inactive 🤖 Prompt for AI Agents |
||
|
|
||
| async def publish(self, project_id: str, event: ProjectEvent) -> None: | ||
| async with self._lock: | ||
| buf = self._replay.setdefault(project_id, deque(maxlen=self._replay_size)) | ||
| buf.append(event) | ||
| for q in list(self._queues.get(project_id, [])): | ||
| queues = list(self._queues.get(project_id, [])) | ||
| # Backpressure policy: do not block the broker (and every other | ||
| # subscriber) on a slow consumer. If a subscriber's bounded queue is | ||
| # full, evict its oldest item and retry so the consumer stays on the | ||
| # live stream rather than stalling indefinitely. | ||
| for q in queues: | ||
| try: | ||
| q.put_nowait(event) | ||
| except asyncio.QueueFull: | ||
| try: | ||
| q.get_nowait() | ||
| except asyncio.QueueEmpty: | ||
| pass | ||
| try: | ||
| q.put_nowait(event) | ||
| except asyncio.QueueFull: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Silent event loss on full queue When a subscriber's bounded queue is full after evicting the oldest item, the event is silently dropped. This could lead to data loss for slow consumers if the queue remains full (e.g., due to concurrent publishers). Reply with |
||
| pass | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove both Markdown fences from this changelog fragment.
The collator treats the opening fence as an
### Addedentry and the closing fence as a### Fixedentry. The rendered changelog therefore places the heading and bullets inside a literal code block. Store raw Markdown, as inchangelog.d/tsk-t5bup2-fix-routing-validation-events.md.🤖 Prompt for AI Agents