Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/buzz-push-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ uuid = { workspace = true }
[dev-dependencies]
openssl = { version = "0.10", features = ["vendored"] }
ciborium = "0.2"
percent-encoding = "2.3"
proptest = { workspace = true }
reqwest = { workspace = true }
37 changes: 37 additions & 0 deletions crates/buzz-push-gateway/schema/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- Push gateway desired-state database manifest.
--
-- The gateway's authority store is deployment-global and lives in its own
-- dedicated database, outside relay community tenancy. This directory declares
-- its end state; `../migrations/` remains the incremental path the gateway
-- binary applies at startup.
-- `src/postgres/desired_schema_postgres_tests.rs` proves that, applied through
-- `pgschema`, the two produce the same `public` tables, columns, table options,
-- constraints, and indexes. That is not full SchemaBot support: the inline FK
-- in `push_gateway_delegations.sql` must be revisited before SchemaBot adopts
-- this schema.
--
-- `tables/public/<table>.sql` declares one table per file: its CREATE TABLE
-- and the CREATE INDEX statements on it, the one-table-per-file shape SchemaBot
-- reads. `tables/` is the SchemaBot schema directory and `public` is its
-- namespace, so nothing else may live under it. The gateway has no types,
-- functions, triggers, or seed rows. Its pre-migration bootstrap guard and
-- runtime-role grants are operational steps in `src/postgres.rs`, not schema.
-- It also has nothing pgschema cannot express (no seed DML, no storage
-- parameters), so the relay's `scripts/reconcile-schema-after-pgschema.sql`
-- step (root `AGENTS.md` gotcha #7) does not apply to it.
--
-- `./bin/pgschema apply --file crates/buzz-push-gateway/schema/schema.sql`
-- resolves the `\i` includes relative to this file. Raw `psql` resolves `\i`
-- against the working directory instead, so run it from here:
-- `cd crates/buzz-push-gateway/schema && psql -f schema.sql`.
--
-- Adding a table: add `tables/public/<table>.sql` and include it below after
-- the tables it references. This file holds only `\i` lines. The layout
-- test in `src/postgres/desired_schema_postgres_tests.rs` enforces both.

\i tables/public/push_gateway_challenges.sql
\i tables/public/push_gateway_installations.sql
\i tables/public/push_gateway_delegations.sql
\i tables/public/push_gateway_endpoint_quotas.sql
\i tables/public/push_gateway_delivery_auth_replays.sql
\i tables/public/push_gateway_delivery_request_replays.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- Single-use App Attest enrollment challenges. Only the hash is stored.
CREATE TABLE push_gateway_challenges (
id UUID PRIMARY KEY,
challenge_hash BYTEA NOT NULL CHECK (length(challenge_hash) = 32),
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX push_gateway_challenges_expiry ON push_gateway_challenges (expires_at);
Comment thread
TheSentinel454 marked this conversation as resolved.
Resolved

-- The unauthenticated challenge route applies a deployment-global rolling
-- issuance quota. Keep its count query bounded as challenge volume grows.
CREATE INDEX push_gateway_challenges_created_at ON push_gateway_challenges (created_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- An installation's delegation to one relay signing key. Installations may
-- authorize multiple relay deployments, one row per relay key.
CREATE TABLE push_gateway_delegations (
id UUID PRIMARY KEY,
installation_id UUID NOT NULL REFERENCES push_gateway_installations(id),
relay_pubkey BYTEA NOT NULL CHECK (length(relay_pubkey) = 32),
endpoint_epoch BIGINT NOT NULL CHECK (endpoint_epoch > 0),
generation BIGINT NOT NULL CHECK (generation > 0),
not_before TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (installation_id, relay_pubkey),
CHECK (not_before < expires_at)
);

CREATE INDEX push_gateway_delegations_expiry ON push_gateway_delegations (expires_at) WHERE revoked_at IS NULL;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Replay ledger for relay delivery auth events, retained until expiry.
CREATE TABLE push_gateway_delivery_auth_replays (
relay_pubkey BYTEA NOT NULL CHECK (length(relay_pubkey) = 32),
auth_event_id BYTEA NOT NULL CHECK (length(auth_event_id) = 32),
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (relay_pubkey, auth_event_id)
);

CREATE INDEX push_gateway_delivery_auth_replays_expiry ON push_gateway_delivery_auth_replays (expires_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Replay ledger for relay delivery request ids, retained until expiry.
CREATE TABLE push_gateway_delivery_request_replays (
relay_pubkey BYTEA NOT NULL CHECK (length(relay_pubkey) = 32),
request_id UUID NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (relay_pubkey, request_id)
);

CREATE INDEX push_gateway_delivery_request_replays_expiry ON push_gateway_delivery_request_replays (expires_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- Per-APNs-token delivery admission window.
CREATE TABLE push_gateway_endpoint_quotas (
token_fingerprint BYTEA PRIMARY KEY CHECK (length(token_fingerprint) = 32),
window_started_at TIMESTAMPTZ NOT NULL,
admitted BIGINT NOT NULL CHECK (admitted >= 0),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX push_gateway_endpoint_quotas_updated ON push_gateway_endpoint_quotas (updated_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- App Attest-bound device installations and their encrypted APNs tokens.
-- Revoked installations remain as retry tombstones until authority expiry, so
-- ownership uniqueness covers live rows only: a replacement can enroll without
-- deleting the old installation's idempotency state.
CREATE TABLE push_gateway_installations (
id UUID PRIMARY KEY,
app_attest_key_id BYTEA NOT NULL CHECK (octet_length(app_attest_key_id) BETWEEN 1 AND 128),
app_attest_public_key BYTEA NOT NULL CHECK (octet_length(app_attest_public_key) BETWEEN 33 AND 256),
assertion_counter BIGINT NOT NULL CHECK (assertion_counter BETWEEN 0 AND 4294967295),
token_ciphertext BYTEA NOT NULL CHECK (octet_length(token_ciphertext) BETWEEN 1 AND 2048),
token_fingerprint BYTEA NOT NULL CHECK (length(token_fingerprint) = 32),
endpoint_epoch BIGINT NOT NULL CHECK (endpoint_epoch > 0),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX push_gateway_installations_expiry ON push_gateway_installations (expires_at) WHERE revoked_at IS NULL;

CREATE UNIQUE INDEX push_gateway_installations_active_app_attest_key ON push_gateway_installations (app_attest_key_id) WHERE revoked_at IS NULL;

CREATE UNIQUE INDEX push_gateway_installations_active_token ON push_gateway_installations (token_fingerprint) WHERE revoked_at IS NULL;
35 changes: 18 additions & 17 deletions crates/buzz-push-gateway/src/postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,23 @@ use uuid::Uuid;
#[path = "postgres/bootstrap_tests.rs"]
mod bootstrap_postgres_tests;

#[cfg(test)]
#[path = "postgres/desired_schema_postgres_tests.rs"]
mod desired_schema_postgres_tests;

static GATEWAY_MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./migrations");

/// Every gateway authority table. Readiness probes and runtime-role grants
/// cover exactly this set; the desired-state parity test pins it to `schema/`.
const GATEWAY_TABLES: [&str; 6] = [
"push_gateway_challenges",
"push_gateway_installations",
"push_gateway_delegations",
"push_gateway_endpoint_quotas",
"push_gateway_delivery_auth_replays",
"push_gateway_delivery_request_replays",
];

#[cfg(test)]
async fn migrate_gateway_through(
pool: &PgPool,
Expand Down Expand Up @@ -58,14 +73,8 @@ impl PostgresAuthorityStore {
REVOKE CREATE ON SCHEMA public FROM {role};
GRANT CONNECT ON DATABASE {database} TO {role};
GRANT USAGE ON SCHEMA public TO {role};
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE
push_gateway_challenges,
push_gateway_installations,
push_gateway_delegations,
push_gateway_endpoint_quotas,
push_gateway_delivery_auth_replays,
push_gateway_delivery_request_replays
TO {role};"
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE {tables} TO {role};",
tables = GATEWAY_TABLES.join(", "),
);
sqlx::raw_sql(AssertSqlSafe(grants)).execute(pool).await?;
Ok(())
Expand All @@ -87,16 +96,8 @@ fn bytes32(v: Vec<u8>) -> Result<[u8; 32], AuthorityError> {
#[async_trait]
impl AuthorityStore for PostgresAuthorityStore {
async fn ready(&self) -> Result<(), AuthorityError> {
const TABLES: [&str; 6] = [
"push_gateway_challenges",
"push_gateway_installations",
"push_gateway_delegations",
"push_gateway_endpoint_quotas",
"push_gateway_delivery_auth_replays",
"push_gateway_delivery_request_replays",
];
let mut tx = self.pool.begin().await.map_err(db)?;
for table in TABLES {
for table in GATEWAY_TABLES {
let ready: bool = sqlx::query_scalar(
"SELECT to_regclass($1) IS NOT NULL
AND COALESCE(has_table_privilege(current_user, to_regclass($1), 'SELECT'), false)
Expand Down
Loading
Loading