Skip to content

Raw db in custom route

The corpus prior

LLM training data is saturated with web handlers that talk to the database directly: cur.execute("DELETE FROM ..."), db.query(Model).filter(...), a hand-rolled repository constructed inline. In a plain Flask/FastAPI app that is idiomatic. So when asked to write a custom Dazzle route — "implement my own POST /encumbrances with bespoke logic" — a model reaches for the same shape: open a connection, run SQL, return. It works at request time, which makes the wrong shape feel correct.

Wrong shape

# dazzle:route-override DELETE /encumbrances/{id}
async def handler(request, id: str):
    async with get_conn() as c:                 # ← raw DB
        await c.execute("DELETE FROM Encumbrance WHERE id = %s", (id,))
    return {"deleted": True}

This handler replaces the generated, permit/scope-bound DELETE /encumbrances/{id} with one that runs no permit check and no scope predicate. Any role that can reach the route can delete any row — the entity's permit:/scope: rules are silently inert. It is outside the RBAC matrix, so the security model can no longer be proven.

Right shape

Bind the handler to the entity + op so the framework runs permit/scope first. Either declare it statically (preferred — scannable, lands in the RBAC matrix):

# dazzle:route-override DELETE /encumbrances/{id}
# dazzle:implements Encumbrance.delete via id      # ← framework wraps with check_entity_op
async def handler(request, id: str):
    ...                                            # body runs ONLY if permit + scope pass

…or call the imperative gate in the body (for body-shaped ops / composite ids):

from dazzle.http.runtime.policy import check_entity_op

async def handler(request, id: str):
    await check_entity_op(request, "Encumbrance", "delete", row_id=id)  # 403/404 on denial
    ...

Let the framework's Repository + policy do the data access; the handler carries only the novel logic. If you genuinely need a generated route gone, suppress it with the entity's expose: allowlist (#1420 Slice 2) — don't shadow it with an un-gated override.

Why this matters here

ADR-0040's invariant is that no route touching a domain entity exists outside that entity's permit/scope model, and every domain route is an RBAC-matrix row. Raw DB access in a custom handler is the way that invariant breaks: it's a write path the matrix can't see and the permit algebra can't reach. expose:/the conformance check govern the declared surface; this counter-prior governs the handler body, the one part structure can't constrain. The scan_handler_for_raw_db filter (route_overrides.py) flags the shapes above so the gap is caught at authoring, not discovered as a prod RBAC bypass.