Ownership

Scope reads and writes to "rows the authenticated user owns" with a single attribute.

Why

A common mistake on multi-user APIs is letting user A read or mutate user B's data because the URL has a row id and the handler trusts it. The fix is to always filter reads and writes by the owner column. 0-mcp makes this declarative.

Usage

class TodoResource(BaseResource):
    model = Todo
    owner_field = 'owner_id'   # column on Todo that holds the user id

Other rows return 404 — indistinguishable from "row does not exist", which is the right answer for an attacker. The same scoping applies to MCP tool calls — an agent authenticated for user A cannot read or mutate user B's row.

🤖 MCP — tools/call

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "delete_todo",
    "arguments": {"id": 42}
  }
}

🌐 REST

GET    /todos
GET    /todos/42
PATCH  /todos/42
DELETE /todos/42

Behaviour

StateBehaviour
owner_field not setNo filtering — handler operates on the row by id
owner_field set, no authenticated user401 (cannot determine ownership)
owner_field set, user owns rowOperation proceeds
owner_field set, user does not own row404

Reads

owner_field filters both reads and writes. Detail GET, list GET, PATCH and DELETE all respect the owner column automatically:

class TodoResource(BaseResource):
    model = Todo
    owner_field = 'owner_id'

    # No extra queryset_filter needed for ownership scoping.
    # `owner_field` already applies to GET/LIST/PATCH/DELETE.
🔒

owner_field is the cheapest defense against IDOR (insecure direct object reference) bugs in CRUD APIs. Set it on every resource where rows belong to a specific user and the framework will scope GET/LIST/PATCH/DELETE automatically.

0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp