Skip to content

Registrant access-events panel (RDAP spec 13 Surface B) - #186

Open
OlegPhenomenon wants to merge 3 commits into
masterfrom
feat/13-registrant-access-transparency
Open

OlegPhenomenon wants to merge 3 commits into
masterfrom
feat/13-registrant-access-transparency

Conversation

@OlegPhenomenon

@OlegPhenomenon OlegPhenomenon commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

What

Surface B (registrant_center portal) of RDAP spec 13 — registrant access transparency: a new "Who accessed my data" panel on the domain page that lists which authorities have accessed a domain's registration data.

The registry read API (Surface A) is implemented separately on the registry repo's feat/rdap-data-api branch; this PR is the front-end + BFF that consumes it.

API contract consumed

GET /api/v1/registrant/domains/:uuid/access_events200 with a JSON array, most-recent-first, each item exactly:

{ "accessed_at": "<ISO-8601 tz-aware>", "organization": "<string|null>", "category": "<string>" }

Empty (owned domain, no disclosable events) → []. Unknown/unowned → 404. Unauthorized → 401. The panel renders only these three fields; nothing else is displayed, logged or stored.

Changes

  • Redux: FETCH_ACCESS_EVENTS_REQUEST/SUCCESS/FAILURE constants; new accessEvents slice keyed by domain uuid with the fetchAccessEvents(uuid) thunk; wired into the store combiner inside withReduxStateSync(combineReducers({...})).
  • API util: api.fetchDomainAccessEvents(uuid).
  • BFF: getDomainAccessEvents handler (mirrors getDomains) forwarding only the session Bearer via API(session), attaching no client-supplied identity; route registered below the app.all('/api/*', API.checkAuth) session gate. Reuses handleResponse unchanged — no response-body or token logging.
  • UI: src/components/DomainAccessEvents/ — a self-contained .page--block panel rendered from DomainPage.jsx, following the portal's existing conventions:
    • DD.MM.Y HH:mm timestamps via moment, as in DomainList / UserData (the API's ISO-8601 value is never shown raw);
    • translated category labels with a circular colour dot, mirroring DomainStatuses; an unrecognised category falls back to its raw value so a registry-side addition surfaces as data rather than vanishing;
    • a translated fallback when organization is null;
    • semantic <Table.Header> (Institution / Category / Access time) with uniquely-keyed rows — the registry does not dedup, so identical events can repeat;
    • four distinct states: loader / Message + retry / empty-state paragraph in the block header (like the nameserver and DNSSEC blocks) / table. An in-flight or failed load never renders as "no authority accessed your data".
  • i18n: domain.accessEvents.* keys (incl. one label per RdapPrivilegeGrant::CATEGORIES value and the institution fallback) added to both en.json and et.json.
  • Tests (Vitest): reducer slice (request/success/failure, keyed-by-uuid, logout reset); panel component (translated categories, date format, institution fallback, unknown category, three fields only, a11y header, all four states); DomainPage integration incl. the registrant gate; i18n key-set match across locales; BFF handler (session Bearer forwarded, no client identity, no body/token logging) and route-below-gate ordering.

Audience: the panel is shown only to the domain's direct registrant

The registry scopes events to Contact.registrant_user_direct_contacts — the ident-matched private-person registrant — but current_registrant_user.domains grants domain access to tech/admin contacts and company representatives as well. Those viewers therefore receive 200 [], which the panel would have stated as "no authority has accessed this domain's data". That is a factual claim we cannot make for them, in a feature whose entire purpose is transparency.

So the panel — and the request — happens only for the direct registrant. This mirrors the spec's own under-disclose posture (RegistrantAccessEventsQuery#build_intervals), adds no user-facing copy while the ET/EN wording is still pending sign-off, and reverses with one condition.

For Timo, on return: spec 13 plan.md:21-23 defers company-representative tenure as an "additive follow-up", and grounding.md:579-583 leaves the breadth of own_ids explicitly open. Two things to decide together:

  1. Should own_ids widen to registrant_user_contacts so company representatives see their company's access history? The gate here widens with it.
  2. Privileged RDAP disclosure also reveals admin/tech contact PII (spec 05), so those contacts arguably have a data-subject interest of their own — but Surface A has no notion of serving them. That is a registry-side scope question, not a UI one.

Tests

npm run test (lint + vitest) — 36 files, 267 tests passing. eslint --max-warnings=0 clean.

Notes / open items

  • The ET/EN empty-state, title, tooltip and category copy is provisional, pending final sign-off. Tests assert behaviour and that keyed messages exist in both locales, not the verbatim wording — so the copy can be finalized without touching the tests.
  • Scope is intentionally limited to the per-domain endpoint (no account-wide view this release).
  • Not verified in a running browser: the portal needs the registry backend on feat/rdap-data-api plus an eeID session. Visual confirmation still to be done on a deployed/dev stack.

Add the "Who accessed my data" transparency panel to the domain page,
showing which authorities have accessed a domain's registration data.

- Redux accessEvents slice (keyed by domain uuid) + thunk fetchAccessEvents,
  wired into the store combiner and the three action-type constants.
- api.fetchDomainAccessEvents(uuid) and the BFF getDomainAccessEvents handler,
  registered below the session-auth gate. The handler forwards only the
  registrant's session Bearer and attaches no client-supplied identity;
  ownership is re-derived independently by the registry.
- DomainPage .page--block with a semantic Semantic-UI table (Institution /
  Category / Access time), uniquely-keyed rows, and an explicit empty state.
  Renders only the three fields the API returns (accessed_at, organization,
  category); no withheld field is displayed, logged or stored.
- ET/EN i18n keys under domain.accessEvents.* (provisional copy, pending
  final sign-off).
- Vitest: reducer slice, panel render + a11y + empty state, i18n key-set
  match, BFF handler (session Bearer forwarded, no body/token logging), and
  route-below-gate ordering.
…ccesses"

Two code-review fixes on the spec-13 Surface B transparency panel.

W1 (DomainPage): the panel showed the empty-state message ("No authority
has accessed this domain's data.") whenever the events list was falsy --
including while the fetch was in flight and permanently after a failure,
a transparency false negative. Render four distinct states instead:
loading (spinner) while in flight, error (message + retry button) on
failure, the empty-state message ONLY on a successful empty array
(Array.isArray && length === 0), and the table when populated. The
withheld fields are still never rendered; only {accessed_at, organization,
category} appear.

W2 (accessEvents reducer): isLoading/error were single top-level flags,
so one domain's in-flight/failed fetch drove another domain's panel.
Scope per-request state by uuid -- state is now { byUuid: { [uuid]:
{ events, isLoading, error } } } -- so loading/error rendering is scoped
to the domain being viewed. events stays undefined until success, letting
the panel tell "loading/failed" apart from "loaded, empty". The fetch
guard keys on the per-uuid record to fetch once and avoid a refetch loop.

i18n: add domain.accessEvents.error and domain.accessEvents.retry to
en.json and et.json (provisional copy, pending sign-off).

Tests: reducer per-uuid scoping (a failure for A leaves B untouched);
panel renders loading while in flight, error+retry on failure (not the
empty message), and the empty message only on a successful empty array.
The panel shipped as a working stub: raw ISO timestamps, the raw category
enum, a bare "-" for a missing institution, and 100 lines of JSX inline in
DomainPage.

Move it into src/components/DomainAccessEvents/ alongside the other feature
components, and render it the way the rest of the portal renders things:
DD.MM.Y HH:mm dates (moment, as in DomainList/UserData), translated category
labels with a circular colour dot mirroring DomainStatuses, a translated
fallback for an unrecorded institution, a semantic Message for the error
state, and the empty state as a header paragraph like the nameserver and
DNSSEC blocks. The four states stay distinct: an in-flight or failed load
still never renders as "no authority accessed your data".

Show the panel only to the domain's direct registrant. The registry scopes
events to Contact.registrant_user_direct_contacts, but grants domain access
to tech/admin contacts and company representatives too, so those viewers get
200 with an empty array — which the panel would have stated as "no authority
has accessed this domain's data". We cannot know that for them, so neither
the panel nor the request happens on their behalf. Widen this together with
own_ids when the registry widens it.

An unrecognised category renders its raw value rather than disappearing, so
a registry-side addition surfaces as data.
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