Skip to content

Commit 096bd30

Browse files
committed
docs(proposal): body-aware policies
1 parent 4d00579 commit 096bd30

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Body-aware policies
2+
3+
Status: Proposed (draft PR, design doc only).
4+
Tracking: ROADMAP.md → Near term.
5+
6+
## Motivation
7+
8+
Today zopa decides allow/deny purely from request headers. That covers
9+
authn-style checks ("Authorization must match a SPIFFE pattern",
10+
"method must be GET"), but it can't reason about the body.
11+
12+
Use cases that need body access:
13+
14+
- Reject requests where a JSON body field falls outside a numeric range
15+
(`input.body.amount > 10000` → deny).
16+
- Block form posts that lack a CSRF nonce field.
17+
- Refuse requests whose body matches a deny-listed substring (cheap WAF).
18+
19+
Currently `proxy_on_request_body` is a no-op (`src/proxy_wasm.zig`)
20+
because by the time it fires, the request pseudo-headers (`:method`,
21+
`:path`, `:authority`) have already been cleared from the header map.
22+
A rule that references both `input.method` and `input.body.amount`
23+
cannot be evaluated in either callback alone.
24+
25+
## Goals
26+
27+
1. In `proxy_on_request_headers`, snapshot `:method`, `:path`,
28+
`:authority`, and a configurable subset of request headers into
29+
per-context state.
30+
1. Implement `proxy_on_request_body`:
31+
- Wait for `end_of_stream` (or buffer up to `max_body_bytes`).
32+
- Read the body via `proxy_get_buffer_bytes(BufferType.HttpRequestBody)`.
33+
- Build an `input` JSON containing snapshot + parsed body.
34+
- Run `evaluate` against the configured policy AST.
35+
- On deny, call `proxy_send_local_response(403)` and return Pause;
36+
on allow, return Continue.
37+
1. Add an opt-in plugin config flag `require_body_eval: true` so hosts
38+
that don't need body inspection don't pay the buffering cost.
39+
40+
## Non-goals
41+
42+
- Streaming evaluation (tracked separately in
43+
`streaming-evaluation.md`). v1 buffers up to `max_body_bytes`.
44+
- Mutating the body. zopa stays decision-only.
45+
- Binary body parsers (protobuf, msgpack). v1 is JSON-only via
46+
`src/json.zig`. Other shapes get the raw byte slice as
47+
`input.body_raw`.
48+
49+
## Design sketch
50+
51+
### Per-context state
52+
53+
```zig
54+
const RequestContext = struct {
55+
method: ?[]const u8 = null,
56+
path: ?[]const u8 = null,
57+
authority: ?[]const u8 = null,
58+
headers: ?json.Value = null,
59+
};
60+
```
61+
62+
A small `AutoHashMap(u32, *RequestContext)` keyed by `context_id` lives
63+
in `host_allocator`. Cleared on `proxy_on_done`.
64+
65+
### Input shape
66+
67+
```json
68+
{
69+
"method": "POST",
70+
"path": "/orders",
71+
"headers": { "...": "..." },
72+
"body": { "amount": 250 },
73+
"body_raw": "{\"amount\":250}"
74+
}
75+
```
76+
77+
`body` is present iff the body parsed as JSON. `body_raw` is always
78+
present once body eval ran.
79+
80+
### Buffer limit
81+
82+
Configurable via plugin config: `max_body_bytes` (default 64 KiB). When
83+
exceeded, the policy sees `body: undefined` and `body_raw` truncated.
84+
Mirrors Envoy's own `max_request_bytes` posture.
85+
86+
## API impact
87+
88+
- `proxy_on_request_body` returns `Action.Pause` until evaluation
89+
completes. Behavior change, but only when the host opts in via
90+
`require_body_eval: true`.
91+
- New AST refs become valid: `input.body.<path>`, `input.body_raw`.
92+
93+
## Test plan
94+
95+
- Node integration test: drive `evaluate` with a synthetic input that
96+
includes `body`, verify `ref` resolves into the body subtree.
97+
- Envoy integration test: extend `examples/envoy/run.sh` with a POST
98+
case that depends on a body field.
99+
- wasmtime test: simulate `proxy_on_request_headers` then
100+
`proxy_on_request_body`, check the snapshot survives between
101+
callbacks.
102+
103+
## Open questions
104+
105+
- How to surface non-JSON bodies (`application/x-www-form-urlencoded`,
106+
binary protocols)? Either a small parser in `src/json.zig` or push
107+
the burden to the host through a richer input ABI.
108+
- Right default for `max_body_bytes`? 64 KiB feels small for GraphQL,
109+
large for control-plane chatter.
110+
- Should `require_body_eval` be inferred from the policy AST (does it
111+
reference `input.body`)? Static AST analysis would flip the flag
112+
ergonomically.

0 commit comments

Comments
 (0)