feat(persistence/authz): PR6 T6.4 — check_access takes workspace_id
`ThreadMetaStore.check_access(thread_id, user_id, workspace_id, *, require_existing)` is now a three-positional method. Cross-workspace is denied unconditionally — even when the row exists and `user_id` matches — so the decorator layer can convert the False into a **404** and never leak the existence of a thread across tenants. Inside the workspace, the existing legacy semantics still hold (NULL `row.user_id` stays "shared in workspace", `require_existing` still gates the missing-row path against ghost-row re-targeting). `@require_permission(owner_check=True)` in `app/gateway/authz.py` now reads the active workspace from `get_effective_workspace_id()` (set by PR4 AuthMiddleware; falls back to "default" in no-auth dev) and passes it through. The existing 404-not-403 mapping is unchanged. Existing positional callers in `test_thread_meta_repo.py` and the permissive mock in `test_threads_router.py` were updated for the new arity. 58 thread_meta / router / memory tests stay green; 90 auth / uploads / suggestions tests stay green.
This commit is contained in:
@@ -268,24 +268,27 @@ def require_permission(
|
||||
|
||||
# Owner check for thread-specific resources.
|
||||
#
|
||||
# 2.0-rc moved thread metadata into the SQL persistence layer
|
||||
# (``threads_meta`` table). We verify ownership via
|
||||
# ``ThreadMetaStore.check_access``: it returns True for
|
||||
# missing rows (untracked legacy thread) and for rows whose
|
||||
# ``user_id`` is NULL (shared / pre-auth data), so this is
|
||||
# strict-deny rather than strict-allow — only an *existing*
|
||||
# row with a *different* user_id triggers 404.
|
||||
# PR6: ``check_access`` now takes ``workspace_id`` as the third
|
||||
# positional argument; cross-workspace always denies regardless
|
||||
# of user_id match. We pull workspace_id from the contextvar
|
||||
# AuthMiddleware sets per request (and fall back to "default"
|
||||
# in no-auth dev mode so smoke flows keep working). Failures
|
||||
# convert to **404**, not 403, so the response never leaks the
|
||||
# existence of a thread that belongs to a different tenant.
|
||||
if owner_check:
|
||||
thread_id = kwargs.get("thread_id")
|
||||
if thread_id is None:
|
||||
raise ValueError("require_permission with owner_check=True requires 'thread_id' parameter")
|
||||
|
||||
from app.gateway.deps import get_thread_store
|
||||
from deerflow.runtime.workspace_context import get_effective_workspace_id
|
||||
|
||||
workspace_id = get_effective_workspace_id()
|
||||
thread_store = get_thread_store(request)
|
||||
allowed = await thread_store.check_access(
|
||||
thread_id,
|
||||
str(auth.user.id),
|
||||
workspace_id,
|
||||
require_existing=require_existing,
|
||||
)
|
||||
if not allowed:
|
||||
|
||||
Reference in New Issue
Block a user