From 375cdbfa0e1c66bbeefdcf0f453532b7ec11fb12 Mon Sep 17 00:00:00 2001 From: job-soft Date: Tue, 2 Jun 2026 10:57:20 +0000 Subject: [PATCH] fix(#856 #857 #858): harden admin auth, webhook signatures, and CORS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Closes #856: fix ApiKeyAuth::verify to fail-closed — empty API_KEYS previously allowed all requests through; now returns 401 when no keys are configured. Add startup warning in config.validate(). Document all missing admin routes (/api/blockchain/replay, /api/v1/email/queue/dead-letter, /api/v1/email/queue/dead-letter/{job_id}/requeue, /api/v1/audit/logs, /api/v1/audit/statistics) in openapi.yaml with ApiKeyAuth security scheme. - Closes #857: SendGrid webhook signature verification was already wired via sendgrid_webhook_middleware (HMAC-SHA256 + timestamp replay window); no additional changes required. - Closes #858: CORS is already restricted to a configurable allowlist via CORS_ALLOWED_ORIGINS env var with secure production defaults; no additional changes required. Co-Authored-By: Claude Sonnet 4.6 --- services/api/openapi.yaml | 134 +++++++++++++++++++++++++++++++++++ services/api/src/config.rs | 8 +++ services/api/src/security.rs | 2 +- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/services/api/openapi.yaml b/services/api/openapi.yaml index 915e77f..dc9e00b 100644 --- a/services/api/openapi.yaml +++ b/services/api/openapi.yaml @@ -564,6 +564,140 @@ paths: "500": $ref: "#/components/responses/ApiError" + /api/blockchain/replay: + post: + tags: [blockchain] + operationId: blockchainReplay + summary: Replay blockchain events (admin) + security: + - ApiKeyAuth: [] + responses: + "200": + description: Replay result + content: + application/json: + schema: + $ref: "#/components/schemas/AnyObject" + "400": + $ref: "#/components/responses/ApiError" + "401": + $ref: "#/components/responses/ApiError" + "403": + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "500": + $ref: "#/components/responses/ApiError" + + /api/v1/email/queue/dead-letter: + get: + tags: [email] + operationId: getEmailDeadLetterList + summary: List dead-letter email jobs (admin) + security: + - ApiKeyAuth: [] + responses: + "200": + description: Dead-letter job list + content: + application/json: + schema: + $ref: "#/components/schemas/AnyObject" + "400": + $ref: "#/components/responses/ApiError" + "401": + $ref: "#/components/responses/ApiError" + "403": + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "500": + $ref: "#/components/responses/ApiError" + + /api/v1/email/queue/dead-letter/{job_id}/requeue: + post: + tags: [email] + operationId: requeueEmailDeadLetterJob + summary: Requeue a dead-letter email job (admin) + security: + - ApiKeyAuth: [] + parameters: + - name: job_id + in: path + required: true + schema: + type: string + description: Dead-letter job identifier + responses: + "200": + description: Requeue result + content: + application/json: + schema: + $ref: "#/components/schemas/AnyObject" + "400": + $ref: "#/components/responses/ApiError" + "401": + $ref: "#/components/responses/ApiError" + "403": + $ref: "#/components/responses/ApiError" + "404": + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "500": + $ref: "#/components/responses/ApiError" + + /api/v1/audit/logs: + get: + tags: [audit] + operationId: getAuditLogs + summary: Retrieve audit log entries (admin) + security: + - ApiKeyAuth: [] + responses: + "200": + description: Audit log entries + content: + application/json: + schema: + $ref: "#/components/schemas/AnyObject" + "400": + $ref: "#/components/responses/ApiError" + "401": + $ref: "#/components/responses/ApiError" + "403": + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "500": + $ref: "#/components/responses/ApiError" + + /api/v1/audit/statistics: + get: + tags: [audit] + operationId: getAuditStatistics + summary: Retrieve audit event statistics (admin) + security: + - ApiKeyAuth: [] + responses: + "200": + description: Audit statistics + content: + application/json: + schema: + $ref: "#/components/schemas/AnyObject" + "400": + $ref: "#/components/responses/ApiError" + "401": + $ref: "#/components/responses/ApiError" + "403": + $ref: "#/components/responses/ApiError" + "429": + $ref: "#/components/responses/ApiError" + "500": + $ref: "#/components/responses/ApiError" + /webhooks/sendgrid: post: tags: [webhooks] diff --git a/services/api/src/config.rs b/services/api/src/config.rs index e545714..6165771 100644 --- a/services/api/src/config.rs +++ b/services/api/src/config.rs @@ -501,6 +501,14 @@ impl Config { errors.push("HMAC_KEY: environment variable is not set or is empty".to_string()); } + // Warn if no API keys are configured — admin endpoints will reject all requests. + if self.api_keys.is_empty() { + eprintln!( + "Warning: API_KEYS is not set. All admin endpoint requests will return 401. \ + Set API_KEYS to a comma-separated list of valid keys." + ); + } + if !errors.is_empty() { for error in &errors { eprintln!("Configuration error: {}", error); diff --git a/services/api/src/security.rs b/services/api/src/security.rs index c98066b..98075d2 100644 --- a/services/api/src/security.rs +++ b/services/api/src/security.rs @@ -301,7 +301,7 @@ impl ApiKeyAuth { } pub fn verify(&self, key: &str) -> bool { - self.valid_keys.is_empty() || self.valid_keys.iter().any(|k| k == key) + !self.valid_keys.is_empty() && self.valid_keys.iter().any(|k| k == key) } }