Skip to content

Accept optional visible_project_ids filter on project list endpoint (defense-in-depth for cloud private projects) #720

@jope-bm

Description

@jope-bm

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:

  1. Compute the set of projects the current user can see (from project_permission + project_share tables)
  2. Pass the resulting set as the visible_project_ids parameter on every call into basic-memory
  3. 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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    cloudBasic Memory CloudenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions