From 2a5f8977bb8cd3ce3d70bccda5af217f2628ef93 Mon Sep 17 00:00:00 2001 From: NancyKwongPerplexity Date: Wed, 21 Oct 2026 18:24:32 +0000 Subject: [PATCH] db: add a table of data-plane session attach tokens A data-plane session stores one attach token in data_plane_sessions.token_hash, so replaying the session's create with the same idempotency key overwrites it and the token every earlier response returned stops working at once. Add data_plane_session_tokens, one row per token keyed by its hash, with its own expires_at and removed with its session. An index on (session_id, expires_at) serves per-session pruning and the cascade. The existing columns are unchanged. space_ro is fenced from the new token_hash column like data_plane_sessions.token_hash. Nothing reads or writes the table yet; the API starts using it in a following change. --- .../libs/db/migrator/src/bootstrap_roles.rs | 1 + ...1182104_data_plane_session_tokens.down.sql | 8 +++++++ ...021182104_data_plane_session_tokens.up.sql | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 db/postgres/20261021182104_data_plane_session_tokens.down.sql create mode 100644 db/postgres/20261021182104_data_plane_session_tokens.up.sql diff --git a/crates/libs/db/migrator/src/bootstrap_roles.rs b/crates/libs/db/migrator/src/bootstrap_roles.rs index b781384f66..03efde0af7 100644 --- a/crates/libs/db/migrator/src/bootstrap_roles.rs +++ b/crates/libs/db/migrator/src/bootstrap_roles.rs @@ -110,6 +110,7 @@ const RO_COLUMN_DENYLIST: &[(&str, &[&str])] = &[ ("sessions", &["token_hash"]), ("api_keys", &["key_hash"]), ("data_plane_sessions", &["token_hash"]), + ("data_plane_session_tokens", &["token_hash"]), ("sandbox_federated_service_grants", &["grant_blob"]), ("template_system_publication_grants", &["id"]), ("template_tenant_publication_grants", &["id"]), diff --git a/db/postgres/20261021182104_data_plane_session_tokens.down.sql b/db/postgres/20261021182104_data_plane_session_tokens.down.sql new file mode 100644 index 0000000000..4a8a4c208e --- /dev/null +++ b/db/postgres/20261021182104_data_plane_session_tokens.down.sql @@ -0,0 +1,8 @@ +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '60s'; + +-- Reverting drops every token a replay minted beyond the session's latest, +-- which `data_plane_sessions.token_hash` still holds. Roll the API and +-- internal-api releases back with this migration: a release that writes the +-- table fails its session creates and replays once the table is gone. +DROP TABLE IF EXISTS data_plane_session_tokens; diff --git a/db/postgres/20261021182104_data_plane_session_tokens.up.sql b/db/postgres/20261021182104_data_plane_session_tokens.up.sql new file mode 100644 index 0000000000..cbf3d81297 --- /dev/null +++ b/db/postgres/20261021182104_data_plane_session_tokens.up.sql @@ -0,0 +1,23 @@ +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '60s'; + +-- Live attach tokens of a data-plane session, one row per token. +-- +-- Replaying a session's create with the same idempotency key mints another +-- token for the same session. Each token stays valid until its own +-- `expires_at`, so a replay no longer invalidates the tokens earlier replays +-- returned. `data_plane_sessions.token_hash` and `token_expires_at` keep +-- naming the most recent token while servers that read only those columns are +-- still deployed; validators consult this table first and fall back to them. +CREATE TABLE IF NOT EXISTS data_plane_session_tokens ( + -- SHA-256 hex digest of the bearer token; the raw token is never stored. + token_hash text PRIMARY KEY, + session_id text NOT NULL REFERENCES data_plane_sessions (session_id) ON DELETE CASCADE, + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +-- Replays prune and cap a session's tokens by expiry, and the session +-- foreign key's cascade finds its rows through the leading column. +CREATE INDEX IF NOT EXISTS idx_data_plane_session_tokens_session_id_expires_at + ON data_plane_session_tokens (session_id, expires_at);