ReviewOS

zig-utils/zig-waf

[WAF-48] Define and implement the versioned API v1 OpenAPI contract

#49
Closed chris opened this 9 days ago ·
9 days ago

Objective

Define resources and stable wire contracts for auth, nodes, policies, rules, exclusions, rollouts, events, audits, metrics, alerts, settings, and health.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

The contract is defined and enforced; the socket layer is not written.

Done — resources and stable wire contracts (1dcc1fc)

34 operations across every resource this issue names — auth, nodes, policies, rules, exclusions, rollouts, events, audits, metrics, alerts, settings, and health — as an OpenAPI 3.1 document (src/api-v1.json) and as the route table the code dispatches on (src/api.zig).

Versioned — the version is in the path prefix. A breaking change to any operation takes a new prefix rather than mutating one clients already call.

Authorization is a property of the route, not of a handler. Every operation declares the action it requires, and an identity that cannot perform it is refused before any handler runs. A handler added without a permission check is a hole that reviews miss; a route added without a requires does not compile. Health and login are the only unauthenticated operations — a load balancer has no credentials, and refusing the login is circular — and every other route refuses an unauthenticated caller rather than defaulting to a role.

The document and the code cannot drift. A test cross-checks them: every documented operation must resolve in the table under the same method, and every routable operation must be documented. A published contract clients are written against must not describe an API that does not exist. Verified by renaming an operationId and watching the suite fail.

Also split roles and actions into src/authz.zig, so the route table can name them without pulling libpq into every build that reads it.

Not done — the server

Binding a socket, parsing requests, and writing responses. That is deliberate for now: what had to be fixed first is which operations exist, what each is called, and who may invoke them, because those are what a client depends on and where a mistake is a security hole rather than a bug. Handlers can be written against repositories that already exist (#52–#59) without changing any of the above.

Leaving this open until the handlers and the listener are implemented and tested, rather than closing it on the contract alone.

9 days ago

Closed by 1dcc1fc, 27accc6, 5bd8acb, and b18495f.

The v1 contract is defined and implemented as a route table in src/api.zig with the OpenAPI 3.1 document in src/api-v1.json, held in step by tests that fail the build if the two disagree.

  • 41 operations across every resource in scope: auth and tokens, nodes and enrollment, node identity (enrollment tokens, certificates, rotation, capabilities), policies and their rules and exclusions, rollouts and drift, events and audit search and export, alerts, metrics, settings, and health.
  • Authorization is a property of the operation, declared on the route rather than checked in a handler — a handler added without a check is a hole, while a route added without a requires does not compile.
  • 51 schemas describing every request and response, derived from what the repositories actually store. Requests set additionalProperties: false so a misspelled field is refused rather than silently ignored — a caller who believes they set a webhook signing secret that was never stored has a security problem, not a typo. Responses stay open so adding a field is not a breaking change.
  • One error shape for every failure, reached through shared responses, so a client writes one error path.
  • Path and query parameters declared, with optionality stated rather than left to a generator's default.

Five tests enforce the contract: operations exist in both places, no two routes can match the same path, every path parameter matches its template exactly, every failure references the shared error schema, and every operation describes its payload while no schema goes unreferenced. Each was confirmed to fail on a document that violates it. Redocly lints the result as valid OpenAPI 3.1 with no warnings.

One bug was caught in the process: /nodes/enrollment-tokens and /nodes/certificates/rotation-due are both matched by /nodes/{node_id}, so listing tokens would have been served as a lookup of a node named "enrollment-tokens". Uniqueness of patterns does not imply uniqueness of what they match; both are now top-level resources and a test rejects the whole class.

Serving these operations over a socket is a connector's job and remains with the reverse-proxy work.

Sign in to comment on this issue.