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_share tables)
- Pass the resulting set as the
visible_project_ids parameter 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.
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_permission table 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.
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_everyonerefactor 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_permissiontable), 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_idsfilter 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.The cloud service would:
project_permission+project_sharetables)visible_project_idsparameter on every call into basic-memoryWhy 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:
Parameter format:
Scope of endpoints:
GET /v2/projects/(project list)GET /v2/projects/{id}/...endpoints (defense against resolving an ID the user can't see)Related
Not in Scope
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.