Skip to content

fix: validate API key against pg-pkg, not a local allowlist#139

Merged
rubenhensen merged 1 commit intomainfrom
fix/api-key-validate-via-pkg
May 6, 2026
Merged

fix: validate API key against pg-pkg, not a local allowlist#139
rubenhensen merged 1 commit intomainfrom
fix/api-key-validate-via-pkg

Conversation

@rubenhensen
Copy link
Copy Markdown
Contributor

Summary

Closes #123. Supersedes #130.

The ApiKeyPresent request guard previously only checked that an X-Api-Key header was present — any value (X-Api-Key: x) admitted the caller to the higher-quota tier (100 GB/upload, 100 GB rolling vs. the default 5 GB). Closed PR #130 fixed this with a hashed allowlist in conf/config.toml, but that approach drifts from the source of truth in postguard-business (business_api_keys) every time a key is rotated or revoked.

This PR routes the validation through pg-pkg's new /v2/api-key/validate (encryption4all/postguard#167). Single source of truth, no drift.

What changed

  • ApiKey request guard now reads Authorization: Bearer PG-… and calls pg-pkg's validate endpoint via async reqwest. Returned tenant id flows into FileState.api_key_tenant: Option<String>.
  • FileState also carries api_key_validation_failed: bool for the case where pg-pkg was unreachable for the full retry budget at init time.
  • New Error::ServiceUnavailable (HTTP 503) for when an over-default upload runs into validation_failed=true — see failure modes below.
  • Rolling-window accounting keys on api-key:<tenant> for API-tier callers, falling back to per-sender email otherwise. Same shape as the closed fix: validate X-Api-Key against configured allowlist #130, applied here too.
  • conf/config.toml is unchanged — no [[api_keys]] allowlist anymore.

Failure modes

Per the rule "small uploads degrade silently, large uploads get an explicit error":

Scenario Init result Chunk result
No bearer / non-PG bearer tenant=None, validation_failed=false default tier; 413 if >5 GB
pg-pkg returns 401/403 tenant=None, validation_failed=false default tier; 413 if >5 GB
pg-pkg validates the key tenant=Some(uuid), validation_failed=false API tier; 413 if >100 GB
pg-pkg unreachable for 30 s tenant=None, validation_failed=true <5 GB → continues with warning logged; >5 GB → 503

Retry policy: exponential backoff (250 ms → 5 s ceiling), 30 s total budget, only on connection errors and 5xx. 401/403 short-circuit immediately.

API contract change

Clients now send Authorization: Bearer PG-… instead of X-Api-Key: PG-…. Update callers before deploy (frontend, Outlook add-in, business portal API). Greppable on the old X-Api-Key header name.

Tests

7 new unit tests pin the bearer-extract rules:

  • accepts Bearer PG-… and bearer PG-… (case-insensitive scheme)
  • rejects missing/empty header
  • rejects non-PG token (e.g. JWT)
  • rejects wrong scheme (Basic, etc.)
  • rejects PG-prefix without scheme

Full suite: cargo test37 passed; 0 failed. cargo fmt --check clean.

Deploy order

pg-pkg PR (encryption4all/postguard#167) ships first. If this lands before pg-pkg, every API-key upload would burn the 30 s retry budget, then 503 on over-default uploads / silently degrade on under-default ones.

Test plan

  • CI green (rust quality job: fmt, clippy, test)
  • After both PRs merge, smoke against staging:
    • No Authorization header → uploads up to 5 GB, 413 above
    • Real PG-… key → uploads up to 100 GB, 413 above; rolling window keys on api-key:<tenant>
    • Bogus PG-… key → 401 from pg-pkg, cryptify treats as default tier, 413 above 5 GB
    • pg-pkg stopped → small upload succeeds with warning in cryptify logs; large upload returns 503

)

The original `ApiKeyPresent` request guard only checked that an `X-Api-Key`
header was present, admitting any non-empty value to the higher-quota tier
(100 GB per upload, 100 GB rolling vs. the default 5 GB). Anyone could
defeat the storage/bandwidth abuse controls with `X-Api-Key: anything`.

Supersedes the closed PR #130, which validated against a hashed allowlist
in cryptify's config — that approach drifted from the source of truth in
postguard-business (`business_api_keys`) on every key rotation/revocation.

Now: cryptify reads `Authorization: Bearer PG-…`, calls pg-pkg's new
`GET /v2/api-key/validate`, and uses the returned tenant id (`organizations.id`)
both for limit selection and as the rolling-window accounting key
(`api-key:<tenant>`). The bespoke `X-Api-Key` header is gone — clients now
use the same `Authorization: Bearer PG-…` shape pg-pkg already accepts.

Failure modes per the rule "small uploads degrade silently, large uploads
get an explicit error":

- pg-pkg returns 401/403  → caller is degraded to default tier (their
  expired/fake key just doesn't earn the higher tier).
- pg-pkg unreachable      → retry up to 30s with exponential backoff. If
  still down, mark `api_key_validation_failed=true` on `FileState`. Chunks
  within the default 5 GB cap continue (with a warning logged); chunks
  exceeding it return 503 (cannot apply the higher tier without pg-pkg).
- pg-pkg validates the key → tenant id stored on `FileState`,
  100 GB tier applied, rolling window accounted per tenant.

Tests: 7 new unit tests pin the bearer-extract rules (PG-prefix required,
scheme `Bearer`/`bearer`, rejects missing/empty/wrong-scheme/non-PG
tokens). Full suite: 37 pass.

Depends on the pg-pkg `/v2/api-key/validate` endpoint being deployed first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: X-Api-Key presence-only check allows quota bypass (5GB → 100GB)

1 participant