Skip to content

Internal RDAP data API + privileged-grant store (backend)#2937

Open
OlegPhenomenon wants to merge 6 commits into
masterfrom
feat/rdap-data-api
Open

Internal RDAP data API + privileged-grant store (backend)#2937
OlegPhenomenon wants to merge 6 commits into
masterfrom
feat/rdap-data-api

Conversation

@OlegPhenomenon

Copy link
Copy Markdown
Contributor

Internal RDAP data API + privileged-grant store (backend only)

The public .ee RDAP service must stop reading the registry DB directly and instead consume a registry-side API (lead decision 2026-06-25). RDAP already shipped the consumer side (a client interface + an in-memory mock). This PR is the real API behind that mock.

Spec (authoritative): lives in the RDAP repo — specs/specs/completed/08-registry-side-rdap-api/ (proposal, requirements, api-contract.md, registry-grounding.md). In-repo copy: doc/api/v1/rdap.md.

Endpoints (new namespace /api/v1/internal/rdap/*)

Method · Path Returns
GET …/domains/:name full privileged domain (registrant + admin/tech contacts + glue + DNSSEC)
GET …/registrars/:code {code,name,phone,website} only
GET …/nameservers/:host {hostname,hostname_puny} (DISTINCT-collapsed)
GET …/grants/active?subject= the single active grant, or 404
POST …/grants/:id/touch best-effort last_used_at (204)

What's in it

  • Api::V1::Internal::BaseController + Api::V1::Internal::Rdap::{Domains,Registrars,Nameservers,Grants}Controller.
  • Auth = pre-shared key + IP-allowlist (the authenticate_shared_key pattern; secure_compare; fail-closed when the key is unset). mTLS is a later prod-hardening step. ENV: rdap_internal_api_shared_key, rdap_internal_api_allowed_ips.
  • Net-new RdapPrivilegeGrant model + migration (categories police/cert/ria/eis_internal, statuses active/revoked/suspended, server-authoritative active_for_subject). The registry had no authority-access concept before.
  • Secrets-free Serializers::Rdap::Domain — never emits transfer_code/auth_info/registrant_verification_token (does NOT reuse Serializers::Repp::Domain(sponsored: true), which leaks transfer_code). Returns raw disclosure flags (union) — RDAP applies disclosure policy, not the registry.

Scope

Backend only — no admin UI this PR (grants seeded via fixtures/console; admin CRUD is a later spec).

Tests

minitest integration + model tests. Verified locally inside the docker-images stack (docker exec docker-images-registry-1): 36 runs, 101 assertions, 0 failures, 0 errors. structure.sql updated with only the new table and validated by reloading from scratch (db:test:prepare).

⚠️ Please review before merging. The open questions in doc/api/v1/rdap.md were already resolved with the RDAP lead; the namespace/URL is the one thing easiest to change if you prefer a different home.

The .ee RDAP service must stop reading the registry DB directly and instead
consume a registry-side API (lead decision 2026-06-25). This adds the contract
+ implementation guide for that internal API:

  - GET /api/v1/internal/rdap/domains/:name       (full privileged domain)
  - GET /api/v1/internal/rdap/registrars/:code    (code,name,phone,website)
  - GET /api/v1/internal/rdap/nameservers/:host   (hostname,hostname_puny)
  - GET /api/v1/internal/rdap/grants/active?subject=  (net-new grant store)

Authoritative spec lives in the RDAP repo
(specs/specs/pending/08-registry-side-rdap-api). No code yet; doc only.
Branch off master for review; do not merge to master.
Auth = pre-shared key + IP-allowlist; backend-only (no admin UI this spec);
categories/states confirmed; defaults for namespace/touch/delete-date/disclosure.
New machine-to-machine JSON API consumed by the public RDAP service, so RDAP
no longer reads the registry's normalized DB directly. Namespace
/api/v1/internal/rdap/*, auth = pre-shared key + IP-allowlist.

Endpoints:
- GET domains/:name        full privileged domain (PII + glue + DNSSEC)
- GET registrars/:code     narrow entity {code,name,phone,website}
- GET nameservers/:host    thin {hostname,hostname_puny}, DISTINCT-collapsed
- GET grants/active        the single authoritative-active privilege grant
- POST grants/:id/touch    best-effort last_used_at (204)

Net-new rdap_privilege_grants table + model (active_for_subject resolution,
fail-closed). Secrets-free Serializers::Rdap::Domain (never emits transfer_code/
auth_info/registrant_verification_token). minitest integration + model tests.

Backend only: no admin UI (grants seeded via fixtures/console per the spec).
Without this, a blank/unset shared key makes expected = 'Basic ' and any
caller sending an empty Basic credential would authenticate. Reject when the
key is not configured.
…rar test

structure.sql: only the new rdap_privilege_grants table (CREATE TABLE +
sequence + pkey + 3 indexes) and its schema_migrations version — no unrelated
churn (the noisy full pg_dump regen was discarded). Validated: db:test:prepare
reloads it from scratch and the suite is green (36 runs, 101 assertions, 0F/0E).
registrar happy-path test now asserts full-hash equality (proves the exact
narrow 4-key shape, drops the assert_nil deprecation).
Companion to the RDAP spec 10-rdap-issued-api-token. RDAP owns no database,
so the opaque API tokens it mints persist here and are reached over the
internal RDAP data API — the same seam that already serves domains,
registrars, nameservers and privilege grants.

Implements the six pinned token operations RDAP's registry client calls,
all keyed by the token_hash (RDAP's keyed HMAC-SHA-256 digest of the raw
token; the raw token and the HMAC secret never reach the registry):

  POST   /api/v1/internal/rdap/tokens             create (store hash + metadata)
  GET    /api/v1/internal/rdap/tokens/active       find active by hash (404 if
                                                    revoked/expired/unknown — no
                                                    caller-visible distinction)
  GET    /api/v1/internal/rdap/tokens?subject=     list a subject's tokens
  POST   /api/v1/internal/rdap/tokens/revoke       revoke one (idempotent, 204)
  POST   /api/v1/internal/rdap/tokens/revoke_all   kill-all-by-subject (-> count)
  POST   /api/v1/internal/rdap/tokens/touch        best-effort last_used_at only

- rdap_api_tokens table (token_hash unique, subject+revoked_at index); no
  privilege field — authorization stays 100% grant-driven. Migration +
  hand-spliced structure.sql (schema_format :sql).
- Model RdapApiToken: active_by_hash / for_subject scopes, idempotent revoke!,
  touch_last_used! that never moves expires_at (no sliding renewal).
- Fix (existing): add subject + token_hash to filter_parameters — the grants
  controller already said 'do not log the subject' (a national id) but nothing
  enforced it; now both are filtered out of logs.
- Tests: model + integration (24 new; full internal/rdap suite 60 runs / 158
  assertions / 0 failures via docker-images-registry-1).

Contract mirrors the RDAP-side interface (Rdap::Registry::Client TOKEN_OPERATIONS
+ Token value object) so the RDAP RealClient token methods drop straight onto it.
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.

1 participant