| @@ -0,0 +1,27 @@ | ||
| + | # Oversight live registry configuration. | |
| + | # Copy to .env locally and fill in deployment-specific values. Do not commit .env. | |
| + | ||
| + | # Host ports. The registry stays loopback-only; Caddy owns public TLS. | |
| + | OVERSIGHT_REGISTRY_BIND=127.0.0.1 | |
| + | OVERSIGHT_HTTP_BIND=0.0.0.0 | |
| + | OVERSIGHT_HTTPS_BIND=0.0.0.0 | |
| + | ||
| + | # Public hostnames. Point DNS at the host running the live Caddy profile. | |
| + | OVERSIGHT_REGISTRY_DOMAIN=registry.oversightprotocol.dev | |
| + | OVERSIGHT_BEACON_DOMAIN=b.oversightprotocol.dev | |
| + | OVERSIGHT_OCSP_DOMAIN=ocsp.oversightprotocol.dev | |
| + | OVERSIGHT_LICENSE_DOMAIN=lic.oversightprotocol.dev | |
| + | ||
| + | # Browser inspector origins allowed to read registry evidence endpoints. | |
| + | OVERSIGHT_CORS_ORIGINS=https://oversightprotocol.dev,https://www.oversightprotocol.dev,https://oversight-protocol.github.io | |
| + | ||
| + | # Live registry behavior. | |
| + | OVERSIGHT_JURISDICTION=GLOBAL | |
| + | OVERSIGHT_REKOR_ENABLED=0 | |
| + | OVERSIGHT_REKOR_URL=https://log2025-1.rekor.sigstore.dev | |
| + | TRUSTED_PROXY=1 | |
| + | ||
| + | # Secrets. Generate real values locally, for example: | |
| + | # openssl rand -hex 32 | |
| + | OVERSIGHT_DNS_EVENT_SECRET= | |
| + | OVERSIGHT_OPERATOR_TOKEN= |
| @@ -1,5 +1,23 @@ | ||
| # Oversight CHANGELOG | ||
| + | ## Unreleased | |
| + | ||
| + | - **Live registry deployment config.** `docker-compose.yml` now has a `live` | |
| + | Caddy profile with public TLS routing for the registry, beacon, OCSP-style, | |
| + | and license-style hostnames. `Caddyfile` covers the full registry v1 | |
| + | read/evidence/tlog surface plus beacon routes, with all hostnames coming | |
| + | from environment variables. `.env.example` documents public-safe defaults | |
| + | and leaves secrets blank. | |
| + | - **Registry operator token.** The Python reference registry can now require | |
| + | `OVERSIGHT_OPERATOR_TOKEN` for `POST /register` and `POST /attribute`. | |
| + | The token is optional so local development and unauthenticated conformance | |
| + | runs keep working, but production operators can protect write-side APIs | |
| + | without changing route shapes. The conformance harness sends the token as | |
| + | a bearer header when `OVERSIGHT_OPERATOR_TOKEN` is set. | |
| + | - **Deployment docs.** Added `docs/REGISTRY_DEPLOYMENT.md` covering the live | |
| + | Compose/Caddy flow, route map, token headers, DNS bridge secret, and local | |
| + | versus live conformance commands. | |
| + | ||
| ## v0.4.11 - 2026-05-08 Hardware-keys completion: Python parity, browser support, end-to-end seal | ||
| The `OSGT-HW-P256-v1` suite is now implemented end-to-end across all |
| @@ -1,53 +1,107 @@ | ||
| - | # OVERSIGHT registry - production Caddy config. | |
| + | # Oversight registry, production Caddy config. | |
| # | ||
| - | # Replace `oversightprotocol.dev` with the beacon domain you actually own | |
| - | # (the one whose hostname is baked into the beacon URLs you mint). | |
| + | # Set the hostnames in .env before starting the live Compose profile: | |
| + | # OVERSIGHT_REGISTRY_DOMAIN=registry.example.org | |
| + | # OVERSIGHT_BEACON_DOMAIN=b.example.org | |
| + | # OVERSIGHT_OCSP_DOMAIN=ocsp.example.org | |
| + | # OVERSIGHT_LICENSE_DOMAIN=lic.example.org | |
| # | ||
| - | # Beacons look like: | |
| - | # https://b.oversightprotocol.dev/p/{token}.png | |
| - | # https://ocsp.oversightprotocol.dev/r/{token} | |
| - | # https://lic.oversightprotocol.dev/v/{token} | |
| - | # | |
| - | # The simplest setup uses a single apex + path-based routing. Caddy auto-provisions | |
| - | # TLS via Let's Encrypt. | |
| + | # Caddy terminates TLS. The registry service stays private on the Compose | |
| + | # network and is reachable from the host only on the configured loopback bind. | |
| - | oversightprotocol.dev { | |
| - | encode gzip | |
| + | (registry_upstream) { | |
| + | reverse_proxy oversight-registry:8765 | |
| + | } | |
| - | # Attribution / evidence API - lock down with auth in production. | |
| - | handle /register { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | (security_headers) { | |
| + | header { | |
| + | X-Content-Type-Options nosniff | |
| + | Referrer-Policy no-referrer | |
| + | Permissions-Policy interest-cohort=() | |
| } | ||
| - | handle /attribute { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | } | |
| + | ||
| + | {$OVERSIGHT_REGISTRY_DOMAIN:registry.oversightprotocol.dev} { | |
| + | encode zstd gzip | |
| + | import security_headers | |
| + | ||
| + | @registry_read path /health /.well-known/oversight-registry /evidence/* /tlog/* /candidates/semantic | |
| + | handle @registry_read { | |
| + | import registry_upstream | |
| } | ||
| - | handle /evidence/* { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | ||
| + | @registry_write path /register /attribute | |
| + | handle @registry_write { | |
| + | import registry_upstream | |
| } | ||
| - | handle /health { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | ||
| + | @dns_bridge path /dns_event | |
| + | handle @dns_bridge { | |
| + | import registry_upstream | |
| } | ||
| - | # Public beacon endpoints - open to the internet by design. | |
| - | handle /p/* { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | handle { | |
| + | respond 404 | |
| } | ||
| - | handle /r/* { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | ||
| + | log { | |
| + | output file /data/registry-access.log { | |
| + | roll_size 100mb | |
| + | roll_keep 10 | |
| + | } | |
| } | ||
| - | handle /v/* { | |
| - | reverse_proxy oversight-registry:8765 | |
| + | } | |
| + | ||
| + | {$OVERSIGHT_BEACON_DOMAIN:b.oversightprotocol.dev} { | |
| + | encode zstd gzip | |
| + | import security_headers | |
| + | ||
| + | handle /p/* { | |
| + | import registry_upstream | |
| } | ||
| - | # Everything else -> 404 | |
| handle { | ||
| respond 404 | ||
| } | ||
| log { | ||
| - | output file /data/access.log { | |
| + | output file /data/beacon-access.log { | |
| roll_size 100mb | ||
| roll_keep 10 | ||
| } | ||
| } | ||
| } | ||
| + | ||
| + | {$OVERSIGHT_OCSP_DOMAIN:ocsp.oversightprotocol.dev} { | |
| + | encode zstd gzip | |
| + | import security_headers | |
| + | ||
| + | handle /r/* { | |
| + | import registry_upstream | |
| + | } | |
| + | ||
| + | handle /ocsp/r/* { | |
| + | import registry_upstream | |
| + | } | |
| + | ||
| + | handle { | |
| + | respond 404 | |
| + | } | |
| + | } | |
| + | ||
| + | {$OVERSIGHT_LICENSE_DOMAIN:lic.oversightprotocol.dev} { | |
| + | encode zstd gzip | |
| + | import security_headers | |
| + | ||
| + | handle /v/* { | |
| + | import registry_upstream | |
| + | } | |
| + | ||
| + | handle /lic/v/* { | |
| + | import registry_upstream | |
| + | } | |
| + | ||
| + | handle { | |
| + | respond 404 | |
| + | } | |
| + | } |
| @@ -17,6 +17,7 @@ COPY registry/ ./registry/ | ||
| # Persistent data volume | ||
| VOLUME ["/data"] | ||
| ENV OVERSIGHT_DB=/data/oversight-registry.sqlite | ||
| + | ENV OVERSIGHT_DATA=/data | |
| EXPOSE 8765 | ||
| @@ -48,6 +48,25 @@ pip install ".[formats]" | ||
| pip install ".[all]" | ||
| ``` | ||
| + | ## Live Registry Deployment | |
| + | ||
| + | The reference registry ships with a public-safe Compose/Caddy deployment path. | |
| + | Start `oversight-registry` on loopback for local operation, then enable the | |
| + | `live` profile when DNS is ready and Caddy should terminate TLS for the | |
| + | registry, beacon, OCSP-style, and license-style hostnames. | |
| + | ||
| + | ```bash | |
| + | cp .env.example .env | |
| + | docker compose up -d oversight-registry | |
| + | docker compose --profile live up -d | |
| + | ``` | |
| + | ||
| + | Set `OVERSIGHT_DNS_EVENT_SECRET` and `OVERSIGHT_OPERATOR_TOKEN` in `.env` | |
| + | before exposing a public host. The operator token protects `POST /register` | |
| + | and `POST /attribute`; the DNS secret authenticates `/dns_event` bridge | |
| + | callbacks. Full route map and validation commands are in | |
| + | [`docs/REGISTRY_DEPLOYMENT.md`](docs/REGISTRY_DEPLOYMENT.md). | |
| + | ||
| ## Quick start | ||
| ```bash |
| @@ -1,41 +1,52 @@ | ||
| services: | ||
| oversight-registry: | ||
| build: . | ||
| - | image: oversight-registry:0.1.0 | |
| + | image: oversight-registry:0.4.11 | |
| container_name: oversight-registry | ||
| restart: unless-stopped | ||
| ports: | ||
| - | # bind to loopback only by default - put a reverse proxy (Caddy/nginx/Traefik) | |
| - | # in front to terminate TLS and reach the public beacon domain | |
| - | - "127.0.0.1:8765:8765" | |
| + | # Loopback by default. Use the live Caddy profile for public TLS. | |
| + | - "${OVERSIGHT_REGISTRY_BIND:-127.0.0.1}:8765:8765" | |
| volumes: | ||
| - oversight_data:/data | ||
| environment: | ||
| OVERSIGHT_DB: /data/oversight-registry.sqlite | ||
| + | OVERSIGHT_DATA: /data | |
| + | OVERSIGHT_CORS_ORIGINS: ${OVERSIGHT_CORS_ORIGINS:-https://oversightprotocol.dev,https://www.oversightprotocol.dev,https://oversight-protocol.github.io} | |
| + | OVERSIGHT_DNS_EVENT_SECRET: ${OVERSIGHT_DNS_EVENT_SECRET:-} | |
| + | OVERSIGHT_OPERATOR_TOKEN: ${OVERSIGHT_OPERATOR_TOKEN:-} | |
| + | OVERSIGHT_JURISDICTION: ${OVERSIGHT_JURISDICTION:-GLOBAL} | |
| + | OVERSIGHT_REKOR_ENABLED: ${OVERSIGHT_REKOR_ENABLED:-0} | |
| + | OVERSIGHT_REKOR_URL: ${OVERSIGHT_REKOR_URL:-https://log2025-1.rekor.sigstore.dev} | |
| + | TRUSTED_PROXY: ${TRUSTED_PROXY:-1} | |
| healthcheck: | ||
| test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8765/health').read()"] | ||
| interval: 30s | ||
| timeout: 5s | ||
| retries: 3 | ||
| - | # Optional: Caddy TLS terminator + beacon domain fronting. | |
| - | # Uncomment once you have a real domain + DNS pointing at this host. | |
| - | # | |
| - | # caddy: | |
| - | # image: caddy:2-alpine | |
| - | # container_name: oversight-caddy | |
| - | # restart: unless-stopped | |
| - | # ports: | |
| - | # - "80:80" | |
| - | # - "443:443" | |
| - | # volumes: | |
| - | # - ./Caddyfile:/etc/caddy/Caddyfile:ro | |
| - | # - caddy_data:/data | |
| - | # - caddy_config:/config | |
| - | # depends_on: | |
| - | # - oversight-registry | |
| + | caddy: | |
| + | image: caddy:2-alpine | |
| + | container_name: oversight-caddy | |
| + | restart: unless-stopped | |
| + | profiles: ["live"] | |
| + | ports: | |
| + | - "${OVERSIGHT_HTTP_BIND:-0.0.0.0}:80:80" | |
| + | - "${OVERSIGHT_HTTPS_BIND:-0.0.0.0}:443:443" | |
| + | environment: | |
| + | OVERSIGHT_REGISTRY_DOMAIN: ${OVERSIGHT_REGISTRY_DOMAIN:-registry.oversightprotocol.dev} | |
| + | OVERSIGHT_BEACON_DOMAIN: ${OVERSIGHT_BEACON_DOMAIN:-b.oversightprotocol.dev} | |
| + | OVERSIGHT_OCSP_DOMAIN: ${OVERSIGHT_OCSP_DOMAIN:-ocsp.oversightprotocol.dev} | |
| + | OVERSIGHT_LICENSE_DOMAIN: ${OVERSIGHT_LICENSE_DOMAIN:-lic.oversightprotocol.dev} | |
| + | volumes: | |
| + | - ./Caddyfile:/etc/caddy/Caddyfile:ro | |
| + | - caddy_data:/data | |
| + | - caddy_config:/config | |
| + | depends_on: | |
| + | oversight-registry: | |
| + | condition: service_healthy | |
| volumes: | ||
| oversight_data: | ||
| - | # caddy_data: | |
| - | # caddy_config: | |
| + | caddy_data: | |
| + | caddy_config: |
| @@ -0,0 +1,98 @@ | ||
| + | # Registry Deployment | |
| + | ||
| + | This is the public-safe live configuration for the reference Oversight | |
| + | registry. It keeps secrets in `.env`, keeps the registry process off the public | |
| + | host interface, and exposes TLS through Caddy. | |
| + | ||
| + | ## Layout | |
| + | ||
| + | - `Dockerfile` builds the Python reference registry image. | |
| + | - `docker-compose.yml` runs the registry on loopback and adds a `live` profile | |
| + | for Caddy. | |
| + | - `Caddyfile` routes the registry, beacon, OCSP-style, and license-style | |
| + | hostnames to the registry container. | |
| + | - `.env.example` lists every live setting without carrying secret values. | |
| + | ||
| + | ## First Run | |
| + | ||
| + | ```bash | |
| + | cp .env.example .env | |
| + | # Fill OVERSIGHT_DNS_EVENT_SECRET and OVERSIGHT_OPERATOR_TOKEN in .env. | |
| + | # Use high-entropy random values; never commit .env. | |
| + | ||
| + | docker compose up -d oversight-registry | |
| + | curl http://127.0.0.1:8765/health | |
| + | ``` | |
| + | ||
| + | For public TLS, point DNS for the four configured hostnames at the host, then | |
| + | start the live profile: | |
| + | ||
| + | ```bash | |
| + | docker compose --profile live up -d | |
| + | ``` | |
| + | ||
| + | ## Public Routes | |
| + | ||
| + | `OVERSIGHT_REGISTRY_DOMAIN` serves the registry metadata, evidence, tlog, and | |
| + | operator routes: | |
| + | ||
| + | - `GET /health` | |
| + | - `GET /.well-known/oversight-registry` | |
| + | - `GET /evidence/{file_id}` | |
| + | - `GET /tlog/head` | |
| + | - `GET /tlog/proof/{index}` | |
| + | - `GET /tlog/range` | |
| + | - `GET /candidates/semantic` | |
| + | - `POST /register` | |
| + | - `POST /attribute` | |
| + | - `POST /dns_event` | |
| + | ||
| + | The beacon hostnames route only their beacon families: | |
| + | ||
| + | - `OVERSIGHT_BEACON_DOMAIN`: `/p/{token}.png` | |
| + | - `OVERSIGHT_OCSP_DOMAIN`: `/r/{token}` and `/ocsp/r/{token}` | |
| + | - `OVERSIGHT_LICENSE_DOMAIN`: `/v/{token}` and `/lic/v/{token}` | |
| + | ||
| + | Everything else returns `404`. | |
| + | ||
| + | ## Operator Authentication | |
| + | ||
| + | If `OVERSIGHT_OPERATOR_TOKEN` is set, `POST /register` and `POST /attribute` | |
| + | require either: | |
| + | ||
| + | ```http | |
| + | Authorization: Bearer <token> | |
| + | ``` | |
| + | ||
| + | or: | |
| + | ||
| + | ```http | |
| + | X-Oversight-Operator-Token: <token> | |
| + | ``` | |
| + | ||
| + | Leaving `OVERSIGHT_OPERATOR_TOKEN` empty keeps the v1 conformance harness and | |
| + | local development behavior unchanged. Do not leave it empty on a public | |
| + | operator deployment. | |
| + | ||
| + | DNS bridge callbacks are separate. Set `OVERSIGHT_DNS_EVENT_SECRET`; the DNS | |
| + | bridge must send either `Authorization: Bearer <secret>` or | |
| + | `X-Oversight-DNS-Secret: <secret>` when posting `/dns_event`. | |
| + | ||
| + | ## Conformance | |
| + | ||
| + | Local reference check: | |
| + | ||
| + | ```bash | |
| + | python tests/test_registry_conformance.py | |
| + | ``` | |
| + | ||
| + | Live check against a token-protected registry: | |
| + | ||
| + | ```bash | |
| + | OVERSIGHT_REGISTRY_URL=https://registry.example.org \ | |
| + | OVERSIGHT_OPERATOR_TOKEN=<token> \ | |
| + | python tests/test_registry_conformance.py | |
| + | ``` | |
| + | ||
| + | The token is read from the environment and sent as a bearer header. Do not put | |
| + | real token values in shell history on shared machines. |
| @@ -49,6 +49,7 @@ IDENTITY_PATH = DATA_DIR / "registry-identity.json" | ||
| TRUSTED_PROXY = bool(int(os.environ.get("TRUSTED_PROXY", "0"))) | ||
| # When TRUSTED_PROXY=1, honor X-Forwarded-For for rate limiting. | ||
| DNS_EVENT_SECRET = os.environ.get("OVERSIGHT_DNS_EVENT_SECRET", "") | ||
| + | OPERATOR_TOKEN = os.environ.get("OVERSIGHT_OPERATOR_TOKEN", "").strip() | |
| # Rekor v2 wiring (v0.5 Session B). Off by default so existing tests do not | ||
| # generate live network traffic. Set OVERSIGHT_REKOR_ENABLED=1 to opt in. | ||
| @@ -399,6 +400,26 @@ def _rate_limit(request: Request): | ||
| raise HTTPException(429, "rate limit exceeded") | ||
| + | def _bearer_or_header_token(request: Request, header_name: str) -> str: | |
| + | supplied = request.headers.get(header_name, "") | |
| + | if supplied: | |
| + | return supplied.strip() | |
| + | auth = request.headers.get("authorization", "") | |
| + | if auth.lower().startswith("bearer "): | |
| + | return auth[7:].strip() | |
| + | return "" | |
| + | ||
| + | ||
| + | def _require_operator_auth(request: Request): | |
| + | """Require the optional operator bearer token for write-side APIs.""" | |
| + | if not OPERATOR_TOKEN: | |
| + | return | |
| + | supplied = _bearer_or_header_token(request, "x-oversight-operator-token") | |
| + | if hmac.compare_digest(supplied, OPERATOR_TOKEN): | |
| + | return | |
| + | raise HTTPException(401, "operator authentication required") | |
| + | ||
| + | ||
| def _is_loopback_host(host: Optional[str]) -> bool: | ||
| if not host: | ||
| return False | ||
| @@ -411,11 +432,7 @@ def _is_loopback_host(host: Optional[str]) -> bool: | ||
| def _verify_dns_event_auth(request: Request): | ||
| """Authenticate DNS bridge callbacks before trusting client_ip in the body.""" | ||
| if DNS_EVENT_SECRET: | ||
| - | supplied = request.headers.get("x-oversight-dns-secret", "") | |
| - | if not supplied: | |
| - | auth = request.headers.get("authorization", "") | |
| - | if auth.lower().startswith("bearer "): | |
| - | supplied = auth[7:].strip() | |
| + | supplied = _bearer_or_header_token(request, "x-oversight-dns-secret") | |
| if hmac.compare_digest(supplied, DNS_EVENT_SECRET): | ||
| return | ||
| raise HTTPException(401, "invalid DNS event secret") | ||
| @@ -480,6 +497,7 @@ def register(req: RegistrationRequest, request: Request): | ||
| another issuer's attribution record. | ||
| - A per-client rate limit applies. | ||
| """ | ||
| + | _require_operator_auth(request) | |
| _rate_limit(request) | ||
| m = req.manifest | ||
| @@ -636,7 +654,8 @@ async def beacon_license(token_id: str, request: Request): | ||
| @app.post("/attribute") | ||
| - | def attribute(q: AttributionQuery): | |
| + | def attribute(q: AttributionQuery, request: Request): | |
| + | _require_operator_auth(request) | |
| with db() as con: | ||
| row = None | ||
| if q.token_id: |
| @@ -64,17 +64,32 @@ class Client: | ||
| """Thin wrapper that presents the same get/post surface over a | ||
| FastAPI TestClient or a live httpx.Client.""" | ||
| - | def __init__(self, impl, base_url: str = ""): | |
| + | def __init__(self, impl, base_url: str = "", default_headers: Optional[dict[str, str]] = None): | |
| self._impl = impl | ||
| self._base = base_url.rstrip("/") | ||
| + | self._headers = default_headers or {} | |
| + | ||
| + | def _merge_headers(self, kwargs: dict[str, Any]) -> dict[str, Any]: | |
| + | if not self._headers: | |
| + | return kwargs | |
| + | merged = dict(self._headers) | |
| + | merged.update(kwargs.pop("headers", {}) or {}) | |
| + | return {**kwargs, "headers": merged} | |
| def get(self, path: str, **kwargs): | ||
| + | kwargs = self._merge_headers(kwargs) | |
| return self._impl.get(self._base + path, **kwargs) if self._base else self._impl.get(path, **kwargs) | ||
| def post(self, path: str, **kwargs): | ||
| + | kwargs = self._merge_headers(kwargs) | |
| return self._impl.post(self._base + path, **kwargs) if self._base else self._impl.post(path, **kwargs) | ||
| + | def operator_headers() -> dict[str, str]: | |
| + | token = os.environ.get("OVERSIGHT_OPERATOR_TOKEN", "").strip() | |
| + | return {"Authorization": f"Bearer {token}"} if token else {} | |
| + | ||
| + | ||
| def build_in_process_client(): | ||
| """Spin up the reference registry in a fresh temp data dir.""" | ||
| from fastapi.testclient import TestClient | ||
| @@ -102,12 +117,12 @@ def build_in_process_client(): | ||
| server.TLOG = TransparencyLog(server.TLOG_DIR, signing_key_hex=server.IDENTITY["ed25519_priv"]) | ||
| tc = TestClient(server.app) | ||
| - | return Client(tc), tmp, server.IDENTITY["ed25519_pub"] | |
| + | return Client(tc, default_headers=operator_headers()), tmp, server.IDENTITY["ed25519_pub"] | |
| def build_live_client(url: str): | ||
| import httpx | ||
| - | return Client(httpx.Client(timeout=15.0), base_url=url), None, None | |
| + | return Client(httpx.Client(timeout=15.0), base_url=url, default_headers=operator_headers()), None, None | |
| # ---- Manifest fixture -------------------------------------------------------- |
| @@ -187,6 +187,32 @@ def t4_evidence_bundle_can_attach_tlog_proofs(): | ||
| print(" [PASS] evidence bundles attach tlog inclusion proofs for events") | ||
| + | def t5_operator_token_gates_write_side_apis_when_configured(): | |
| + | original_token = registry_server.OPERATOR_TOKEN | |
| + | try: | |
| + | registry_server.OPERATOR_TOKEN = "" | |
| + | registry_server._require_operator_auth(_fake_request("203.0.113.10")) | |
| + | ||
| + | registry_server.OPERATOR_TOKEN = "operator-secret" | |
| + | registry_server._require_operator_auth( | |
| + | _fake_request("203.0.113.10", {"authorization": "Bearer operator-secret"}) | |
| + | ) | |
| + | registry_server._require_operator_auth( | |
| + | _fake_request("203.0.113.10", {"x-oversight-operator-token": "operator-secret"}) | |
| + | ) | |
| + | try: | |
| + | registry_server._require_operator_auth( | |
| + | _fake_request("203.0.113.10", {"authorization": "Bearer wrong"}) | |
| + | ) | |
| + | except HTTPException as exc: | |
| + | assert exc.status_code == 401 | |
| + | else: | |
| + | raise AssertionError("wrong operator token should be rejected") | |
| + | finally: | |
| + | registry_server.OPERATOR_TOKEN = original_token | |
| + | print(" [PASS] optional operator token gates write-side APIs") | |
| + | ||
| + | ||
| def main(): | ||
| print("=" * 60) | ||
| print(" registry.server - focused unit tests") | ||
| @@ -195,8 +221,9 @@ def main(): | ||
| t2_register_rejects_unsigned_sidecar_mismatch() | ||
| t3_dns_event_requires_secret_for_non_loopback() | ||
| t4_evidence_bundle_can_attach_tlog_proofs() | ||
| + | t5_operator_token_gates_write_side_apis_when_configured() | |
| print() | ||
| - | print(" ALL TESTS PASSED - 4/4") | |
| + | print(" ALL TESTS PASSED - 5/5") | |
| if __name__ == "__main__": |