-
Notifications
You must be signed in to change notification settings - Fork 0
feat(api): complete gateway route lifecycle #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
11
commits into
main
Choose a base branch
from
feat/operator-resource-api
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
46bf83e
feat(api): complete gateway route lifecycle
seonghobae a9baf08
fix(api): honor conditional route write semantics
seonghobae 07c5345
docs(api): document route write denial
seonghobae 2892763
fix(api): distinguish readonly management principals
seonghobae 4fc314a
fix(api): accept If-Match entity-tag lists
seonghobae f6b5978
fix(api): require delete precondition before lookup
seonghobae 85a4e43
feat(api): add DNSBL resource lifecycle (#124)
seonghobae 8e7625f
fix(api): authenticate malformed DNSBL writes first
seonghobae 9bb1299
fix(clippy): reduce error variant size in parse_dnsbl_path_address
seonghobae 236cdcd
docs(openapi): align delete contracts with runtime
codex bab3c72
docs(api): describe malformed DNSBL lookup response
codex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # Wardnet HTTP API inventory | ||
|
|
||
| Snapshot: 2026-08-26, `origin/main` at `107117634764c901dff540044585d64088fafedb`. | ||
|
|
||
| | Product area | Existing HTTP contracts | Lifecycle gap after this change | | ||
| | --- | --- | --- | | ||
| | Health and deployment | `GET /healthz`, `/readyz`, `/api/version`, `/metrics`, `/api/support-bundle` | No authenticated runtime configuration view or reload contract. | | ||
| | Gateway and routes | `ANY /gateway/{path}`, `GET/POST /api/routes`, `GET/PUT/DELETE /api/routes/{route_id}`, `POST /api/evaluate` | Route collection pagination and an explicit gateway decision trace lookup remain absent. | | ||
| | WAF | `POST /api/waf/coraza/audit` | No rule-set activation/version API; this must follow the Coraza/CRS authority contract rather than inventing rules. | | ||
| | IDS | `POST /api/ids/suricata/eve` | No sensor registration, sensor health, or EVE cursor/checkpoint API. | | ||
| | AI SOC | `GET /api/soc/llm-config`, `POST /api/soc/analyze` | No analysis job/history/feedback lifecycle. | | ||
| | Events and KPIs | `GET /api/events`, `/api/events.ndjson`, `/api/kpis`, `/api/audit-logs` | Event and audit cursor pagination, time ranges, acknowledgement/case state, and retention controls remain absent. | | ||
| | DNSBL | `GET/POST /api/dnsbl`, `GET/PUT/DELETE /api/dnsbl/{address}`, `GET /dnsbl/zone` | Serial/conditional zone transfer contracts remain absent. | | ||
| | Threat intelligence | `GET /api/threats`, `GET /api/threat-feeds`, `/freshness`; import endpoints for generic feeds, phishing-database, STIX, MISP, TAXII, and OpenCTI | Individual indicator/feed lifecycle and import idempotency keys remain absent. | | ||
| | APIM and load balancing | Route CRUD and prefix-based upstream proxying | No upstream pool/member, health-check, retry/circuit-breaker, API consumer, quota, or API-key lifecycle. These need persisted models before endpoints. | | ||
| | Credentials and config | Admin token RBAC; integration config status views | No credential registry CRUD/rotation metadata API. Secret values must never be returned. Operational config still lacks a durable KV model. | | ||
| | Durability and audit | JSON snapshot persistence and mutation audit rows | No immutable remote audit sink, tenant boundary, or general optimistic-concurrency revision. Route items now use ETag/If-Match. | | ||
|
|
||
| ## Route resource contract | ||
|
|
||
| Legacy `POST /api/routes` remains an upsert and keeps its existing response shape/status. | ||
| New clients should use the item resource: | ||
|
|
||
| - `GET /api/routes/{route_id}` returns the route and an `ETag` header. | ||
| - `PUT /api/routes/{route_id}` creates a missing route. Replacing an existing route requires | ||
| `If-Match` with the latest ETag (`428` when absent, `412` when stale). | ||
| - `DELETE /api/routes/{route_id}` requires `If-Match` and returns `204`. | ||
| - Writes require a write-capable admin principal. An authenticated read-only principal gets | ||
| `403`; a missing or invalid credential gets `401`. Successful replace/delete operations are | ||
| persisted and audited. | ||
|
|
||
| The machine-readable contract is [openapi.yaml](openapi.yaml). | ||
|
|
||
| ## DNSBL resource contract | ||
|
|
||
| - `GET /api/dnsbl/{address}` returns one IPv4 or IPv6 entry and an `ETag` header. | ||
| - `PUT /api/dnsbl/{address}` creates a missing entry. Replacing an existing entry requires | ||
| `If-Match`; the path and body addresses must match. | ||
| - `DELETE /api/dnsbl/{address}` requires `If-Match` and returns `204`. | ||
| - Writes use the same write-capable admin, persistence, rollback, and audit contract as routes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| openapi: 3.1.0 | ||
| info: | ||
| title: Wardnet operator API | ||
| version: 0.1.0 | ||
| description: Operator contract for the route lifecycle. Legacy collection POST remains supported. | ||
| paths: | ||
| /api/routes: | ||
| get: | ||
| summary: List gateway routes | ||
| responses: | ||
| '200': | ||
| description: Route collection | ||
| content: | ||
| application/json: | ||
| schema: | ||
| type: array | ||
| items: { $ref: '#/components/schemas/Route' } | ||
| post: | ||
| summary: Legacy route upsert | ||
| security: [{ AdminToken: [] }] | ||
| requestBody: | ||
| required: true | ||
| content: | ||
| application/json: | ||
| schema: { $ref: '#/components/schemas/Route' } | ||
| responses: | ||
| '201': { description: Route upserted } | ||
| '400': { $ref: '#/components/responses/BadRequest' } | ||
| '401': { $ref: '#/components/responses/Unauthorized' } | ||
| '403': { $ref: '#/components/responses/Forbidden' } | ||
| /api/routes/{route_id}: | ||
| parameters: | ||
| - name: route_id | ||
| in: path | ||
| required: true | ||
| schema: { type: string } | ||
| get: | ||
| summary: Get one route and its concurrency token | ||
| responses: | ||
| '200': | ||
| description: Route | ||
| headers: | ||
| ETag: { schema: { type: string } } | ||
| content: | ||
| application/json: | ||
| schema: { $ref: '#/components/schemas/Route' } | ||
| '404': { $ref: '#/components/responses/NotFound' } | ||
| put: | ||
| summary: Create or conditionally replace one route | ||
| security: [{ AdminToken: [] }] | ||
| parameters: | ||
| - name: If-Match | ||
| in: header | ||
| required: false | ||
| description: Required when the route already exists. | ||
| schema: { type: string } | ||
| requestBody: | ||
| required: true | ||
| content: | ||
| application/json: | ||
| schema: { $ref: '#/components/schemas/Route' } | ||
| responses: | ||
| '200': { description: Route replaced } | ||
| '201': { description: Route created } | ||
| '400': { $ref: '#/components/responses/BadRequest' } | ||
| '401': { $ref: '#/components/responses/Unauthorized' } | ||
| '403': { $ref: '#/components/responses/Forbidden' } | ||
| '412': { description: ETag does not match } | ||
| '428': { description: If-Match required for an existing route } | ||
| delete: | ||
| summary: Conditionally delete one route | ||
| security: [{ AdminToken: [] }] | ||
| parameters: | ||
| - name: If-Match | ||
| in: header | ||
| required: false | ||
| description: Required for every delete; a missing value returns 428. | ||
| schema: { type: string } | ||
| responses: | ||
| '204': { description: Route deleted } | ||
| '401': { $ref: '#/components/responses/Unauthorized' } | ||
| '403': { $ref: '#/components/responses/Forbidden' } | ||
| '412': { description: ETag does not match or route does not exist } | ||
| '428': { description: If-Match required } | ||
| /api/dnsbl/{address}: | ||
| parameters: | ||
| - name: address | ||
| in: path | ||
| required: true | ||
| schema: | ||
| oneOf: | ||
| - { type: string, format: ipv4 } | ||
| - { type: string, format: ipv6 } | ||
| get: | ||
| summary: Get one DNSBL entry and its concurrency token | ||
| responses: | ||
| '200': | ||
| description: DNSBL entry | ||
| headers: | ||
| ETag: { schema: { type: string } } | ||
| '400': { $ref: '#/components/responses/BadRequest' } | ||
| '404': { $ref: '#/components/responses/NotFound' } | ||
|
seonghobae marked this conversation as resolved.
|
||
| put: | ||
| summary: Create or conditionally replace one DNSBL entry | ||
| security: [{ AdminToken: [] }] | ||
| parameters: | ||
| - name: If-Match | ||
| in: header | ||
| required: false | ||
| schema: { type: string } | ||
| responses: | ||
| '200': { description: DNSBL entry replaced } | ||
| '201': { description: DNSBL entry created } | ||
| '400': { $ref: '#/components/responses/BadRequest' } | ||
| '401': { $ref: '#/components/responses/Unauthorized' } | ||
| '403': { $ref: '#/components/responses/Forbidden' } | ||
| '412': { description: ETag does not match } | ||
| '428': { description: If-Match required for an existing entry } | ||
| delete: | ||
| summary: Conditionally delete one DNSBL entry | ||
| security: [{ AdminToken: [] }] | ||
| parameters: | ||
| - name: If-Match | ||
| in: header | ||
| required: false | ||
| description: Required for every delete; a missing value returns 428. | ||
| schema: { type: string } | ||
| responses: | ||
| '204': { description: DNSBL entry deleted } | ||
| '401': { $ref: '#/components/responses/Unauthorized' } | ||
| '403': { $ref: '#/components/responses/Forbidden' } | ||
| '412': { description: ETag does not match or entry does not exist } | ||
| '428': { description: If-Match required } | ||
|
seonghobae marked this conversation as resolved.
coderabbitai[bot] marked this conversation as resolved.
|
||
| components: | ||
| securitySchemes: | ||
| AdminToken: | ||
| type: apiKey | ||
| in: header | ||
| name: X-Admin-Token | ||
| schemas: | ||
| Route: | ||
| type: object | ||
| additionalProperties: false | ||
| required: [id, path_prefix, upstream, mode, enabled] | ||
| properties: | ||
| id: { type: string, minLength: 1 } | ||
| path_prefix: { type: string, pattern: '^/' } | ||
| upstream: { type: string } | ||
| mode: { type: string, enum: [monitor, block] } | ||
| enabled: { type: boolean } | ||
| block_threshold: { type: [integer, 'null'], minimum: 1 } | ||
| Error: | ||
| type: object | ||
| required: [error] | ||
| properties: | ||
| error: { type: string } | ||
| responses: | ||
| BadRequest: | ||
| description: Invalid route or path/body identity mismatch | ||
| content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } | ||
| Unauthorized: | ||
| description: Missing or invalid admin credential | ||
| content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } | ||
| Forbidden: | ||
| description: Authenticated principal is read-only | ||
| content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } | ||
| NotFound: | ||
| description: Route not found | ||
| content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.