-
Notifications
You must be signed in to change notification settings - Fork 179
Accept optional visible_project_ids filter on project list endpoint (defense-in-depth for cloud private projects) #720
Description
Context
This is a defense-in-depth improvement requested by basic-memory-cloud. It's tracked in the cloud side as part of the discussion on basicmachines-co/basic-memory-cloud#518 and is independent of the is_everyone refactor happening there.
Basic Memory Cloud has a concept of per-project visibility in team workspaces — some projects are visible to all workspace members, others are invite-only. Today, the source of truth for visibility lives in the cloud database (project_permission table), and filtering is enforced at the cloud proxy layer by rewriting the project list response after it comes back from basic-memory.
This is fragile. The proxy must catch every code path that could return project data. We've already had one incident (basicmachines-co/basic-memory-cloud#544) where a trailing-slash variant of /v2/projects/ bypassed the filtering route and exposed other users' private projects. The fix was to add defense-in-depth in the catch-all route, but filtering-at-proxy is still a single layer with a wide attack surface: admin tools, backup flows, future endpoints, or any new direct-DB queries all need to re-implement the filter or risk leaking.
Proposal
Add an optional visible_project_ids filter parameter to basic-memory's project list endpoint (GET /v2/projects/). When provided, basic-memory applies the filter in SQL and only returns projects whose IDs are in the set.
GET /v2/projects/?visible_project_ids=uuid1,uuid2,uuid3
The cloud service would:
- Compute the set of projects the current user can see (from
project_permission+project_sharetables) - Pass the resulting set as the
visible_project_idsparameter on every call into basic-memory - basic-memory enforces the filter at query time, not response-rewrite time
Why This Matters
Defense-in-depth: the filter lives in the SQL query, not a response rewriter. URL quirks, routing bugs, or new code paths in the cloud proxy can't bypass it — if the filter isn't passed, basic-memory returns an empty list by convention (or the full list if the parameter is absent, depending on the design choice below).
No migration needed: cloud remains the authoritative source of visibility rules. Nothing moves between databases. basic-memory just gains the ability to honor a filter passed by its caller.
Local-mode compatibility: the parameter is optional. When basic-memory runs in single-user local mode, the parameter is simply not passed and behavior is unchanged — all projects are visible. No new concepts leak into the local product.
Design Questions
-
Parameter semantics when absent:
- Option A: absent parameter → return all projects (current behavior, preserves local mode)
- Option B: absent parameter → return empty list (fail-closed, stronger guarantee for cloud but breaks local mode)
- Recommendation: Option A. The filter is a voluntary restriction a caller can apply. Cloud always passes it; local never does.
-
Parameter format:
- Comma-separated UUIDs in a query param: simple, works with GET
- POST body with a JSON array: cleaner for large sets, but changes the endpoint shape
- Recommendation: comma-separated for simplicity. Workspaces with hundreds of projects are rare, and URL length limits are generous enough.
-
Scope of endpoints:
- Just
GET /v2/projects/(project list) - Also
GET /v2/projects/{id}/...endpoints (defense against resolving an ID the user can't see) - Recommendation: start with project list. The individual-project endpoints already require a specific ID that the caller provides, so the attack surface is smaller. If cloud decides to add per-project filtering later, the same mechanism extends naturally.
- Just
Related
- basicmachines-co/basic-memory-cloud#518 — broader is_everyone refactor discussion
- basicmachines-co/basic-memory-cloud#544 — trailing-slash bypass that motivated this
Not in Scope
- Moving the
project_permissiontable or visibility rules themselves into basic-memory's tenant DB. That's a larger migration with real costs around the local/self-hosted story and is tracked separately. - Adding per-user identity to basic-memory's API contract beyond what already flows through headers.