mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
Stack: this PR → #8377 → #8399 → #8401 bb-public creates communities all the time, so a hand-edited `BUZZ_NIP_FI_COMMUNITIES` map can't list them. This PR stores an explicit NIP-FI issuer allowlist for each community in the database and writes it when the community is created. Login checks don't read the table yet; that comes in #8377 together with the database-backed resolver. Operator steps are in [`docs/operator-nip-fi-bindings.md`](docs/operator-nip-fi-bindings.md). **Storage.** Migration `0066_community_nip_fi_issuers` adds `community_nip_fi_issuers (community_id, issuer, created_at)`, with primary key `(community_id, issuer)` and the community write fence attached. The assertion audience is `https://<communities.host>`, so the table has no `aud` column. Zero rows means no binding. The table is included in whole-community deletion and purge. **One bindable rule.** `BUZZ_NIP_FI_PROVISION_BINDING` (`{"host_suffix": ..., "authorized_issuers": [...]}`) is parsed in `buzz-auth` with `BUZZ_NIP_FI_ISSUERS`, whatever `BUZZ_NIP_FI_MODE` is set to, and both the relay and `buzz-admin` use the same parser. Each authorized issuer must be a configured issuer (1 to 2048 bytes), and the suffix must not cover any host declared in `BUZZ_NIP_FI_COMMUNITIES`; an unreadable declaration refuses startup even in mode `off`. `ProvisionBindingPolicy::is_bindable(host)` is true for a host exactly one label under the suffix that is not reserved. It decides creation-time binding, explicit `bind`, and what the lookup route reports. **Binding on creation.** Rows are written in the same transaction as the community insert on all three creation paths (`POST /operator/communities` `create_only`, its legacy convergence branch, and startup bootstrap), and only when the call actually created the community, so existing rows are never rewritten. **Reserved host.** A host whose `https://<host>` equals a configured `command_audiences` value never holds a binding. Operator provisioning of it is rejected with a 400 and the `reserved_host` error code, bootstrap creates it unbound, and the availability route reports it unavailable with an error code. `main.rs` calls `verify_reserved_hosts_unbound` at startup; if a reserved host has rows the relay refuses to start and names the fix (`buzz-admin nip-fi unbind --host <h> --issuer <i>`, or revert the command audience). A database failure during that check is a separate `Check` error. **Operator commands.** - `buzz-admin nip-fi backfill` pages unbound communities by host (1000 per page) and binds each page with one `INSERT ... SELECT` limited to active communities. It skips communities being deleted, always prints `{rows_bound, unbound, unbound_hosts, truncated}`, and exits non-zero while any bindable community is still unbound. Archived communities are bound on purpose. Both bind paths take the shared deletion lock and then lock the community row (`FOR NO KEY UPDATE`, in id order for a batch), the same order deletion uses, and the backfill insert also requires the community to still have no binding, so an operator `bind` that lands after the backfill listed a community wins and the default issuer is not added beside it. - `buzz-admin nip-fi bind --host --issuer` refuses a reserved host, a host outside the suffix, or an unconfigured issuer. - `buzz-admin nip-fi unbind --host --issuer` refuses to remove a community's last issuer (archive or delete the community instead), with one exception: when a valid provisioning policy shows that backfill can't bind the host (it's reserved, or outside the suffix), the last issuer can be removed. That is the reserved-host recovery path in the runbook. A missing or malformed policy doesn't block `unbind`; it just means the exception never applies. **Lookup route.** `GET /operator/communities/nip-fi-binding?host=` (NIP-98, `RELAY_OPERATOR_PUBKEYS`) reads the database and returns `{host, active, issuers[]}`. `host` is the stored host. It returns 404 for an unknown host, 200 with `active:false` for an archived or quiescing community, and `issuers:[]` when no provisioning policy is set. It lists only issuers that are configured and on a bindable host. kgoose depends on this contract; it is also documented in the runbook. **NIP-FA.** The `relay_url` bullet now also says: "An adapter MAY obtain its exact relay list from a configured, authenticated source; it MUST still compare `relay_url` exactly." **Deployment dependency.** The kgoose config side is already handled by [cash-server#137671](https://github.com/squareup/cash-server/pull/137671). In public-community mode it requires the command audience not to be a community audience. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
213 lines
8.6 KiB
PL/PgSQL
213 lines
8.6 KiB
PL/PgSQL
-- Desired-state database manifest.
|
|
--
|
|
-- `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.
|
|
--
|
|
-- Everything SchemaBot cannot apply lives in this directory's other
|
|
-- subdirectories: `types/`, `functions/`, `partitions/`, `triggers/` (including
|
|
-- write-fence attachment), `seeds/`, and the `community_write_fence.sql`
|
|
-- bootstrap. This file composes all of them into one fresh database, in load
|
|
-- order: types → functions → tables → partitions → triggers → seeds → fence.
|
|
--
|
|
-- `./bin/pgschema apply --file schema/schema.sql` resolves the `\i` includes
|
|
-- relative to this file (it refuses `..`). `pgschema` skips INSERTs and some
|
|
-- storage parameters, so every apply caller must then run
|
|
-- `scripts/reconcile-schema-after-pgschema.sql`. Raw `psql` resolves `\i`
|
|
-- against the working directory instead, so run it from here:
|
|
-- `cd schema && psql -f schema.sql`.
|
|
--
|
|
-- Adding a table: add `tables/public/<table>.sql` and include it below after
|
|
-- the tables it references; put its partitions, triggers, and seed rows in the
|
|
-- matching subdirectories and include those too. This file holds only `\i`
|
|
-- lines and `CREATE EXTENSION`. Outside `tables/public/`, no file may create,
|
|
-- alter or drop a table or index, except `CREATE TABLE ... PARTITION OF` in
|
|
-- `partitions/`. The layout test enforces both.
|
|
|
|
-- Buzz initial Postgres schema — multi-tenant.
|
|
--
|
|
-- Source of truth for fresh database setup. This is a clean, from-scratch
|
|
-- schema in which `community_id` is a first-class, server-resolved key on
|
|
-- every tenant-scoped row. It is NOT additive over the single-community
|
|
-- schema; the rewrite replaces it. Existing single-community deployments
|
|
-- migrate via the documented backfill migration (0002), which assigns all
|
|
-- pre-existing rows to one default community.
|
|
--
|
|
-- The governing contract is docs/multi-tenant-conformance.md. Every table
|
|
-- below cites the conformance surface it implements. The invariant behind the
|
|
-- whole schema (conformance "row zero"): a request's community is resolved
|
|
-- from the connection host by the server, never supplied by the client, and
|
|
-- every scoped row carries that immutable `community_id`.
|
|
--
|
|
-- Migration-lint obligations enforced by the Lane 0 lint harness:
|
|
-- 1. Every tenant-scoped table has `community_id NOT NULL`.
|
|
-- 2. No UNIQUE / PRIMARY KEY / FK on a scoped table is observable across
|
|
-- communities: each leads with `community_id` (or, for child rows whose
|
|
-- parent already pins the community, joins carry the community tuple).
|
|
-- 3. `channels.community_id` is immutable (triggers/channels.sql; no UPDATE path).
|
|
-- 4. Operator-global tables are named in the explicit allowlist, not implied.
|
|
|
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
-- types
|
|
\i types/channel_type.sql
|
|
\i types/channel_visibility.sql
|
|
\i types/member_role.sql
|
|
\i types/workflow_status.sql
|
|
\i types/run_status.sql
|
|
\i types/approval_status.sql
|
|
\i types/delivery_method.sql
|
|
\i types/subscription_status.sql
|
|
\i types/pause_reason.sql
|
|
\i types/channel_add_policy.sql
|
|
|
|
-- functions
|
|
\i functions/channels_community_id_immutable.sql
|
|
\i functions/guard_channel_roster_snapshot.sql
|
|
\i functions/events_created_at_floor_guard.sql
|
|
\i functions/prevent_community_deletion_request_retargeting.sql
|
|
\i functions/prevent_community_deletion_approval_removal.sql
|
|
\i functions/protect_community_deletion_manifest_keys.sql
|
|
\i functions/community_deletion_lock_key.sql
|
|
\i functions/community_write_fence_excluded_table.sql
|
|
\i functions/community_write_allowed.sql
|
|
\i functions/assert_community_write_allowed.sql
|
|
\i functions/enforce_community_write_fence.sql
|
|
\i functions/enforce_community_tombstone.sql
|
|
\i functions/attach_community_write_fence.sql
|
|
\i functions/profile_name_fields.sql
|
|
|
|
-- tables
|
|
\i tables/public/communities.sql
|
|
\i tables/public/channels.sql
|
|
\i tables/public/channel_members.sql
|
|
\i tables/public/users.sql
|
|
\i tables/public/personal_read_accounts.sql
|
|
\i tables/public/personal_read_frontiers.sql
|
|
\i tables/public/events.sql
|
|
\i tables/public/event_mentions.sql
|
|
\i tables/public/subscriptions.sql
|
|
\i tables/public/delivery_log.sql
|
|
\i tables/public/workflows.sql
|
|
\i tables/public/workflow_runs.sql
|
|
\i tables/public/workflow_approvals.sql
|
|
\i tables/public/scheduled_workflow_fires.sql
|
|
\i tables/public/api_tokens.sql
|
|
\i tables/public/rate_limit_violations.sql
|
|
\i tables/public/thread_metadata.sql
|
|
\i tables/public/reactions.sql
|
|
\i tables/public/pubkey_allowlist.sql
|
|
\i tables/public/relay_members.sql
|
|
\i tables/public/join_policy_acceptances.sql
|
|
\i tables/public/relay_invites.sql
|
|
\i tables/public/archived_identities.sql
|
|
\i tables/public/audit_log.sql
|
|
\i tables/public/moderation_actions.sql
|
|
\i tables/public/moderation_reports.sql
|
|
\i tables/public/community_bans.sql
|
|
\i tables/public/community_nip_fi_issuers.sql
|
|
\i tables/public/_operator_global_tables.sql
|
|
\i tables/public/git_repo_names.sql
|
|
\i tables/public/parameterized_event_watermarks.sql
|
|
\i tables/public/product_feedback.sql
|
|
\i tables/public/push_leases.sql
|
|
\i tables/public/push_wake_outbox.sql
|
|
\i tables/public/push_match_queue.sql
|
|
\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
|
|
\i tables/public/replica_heartbeat.sql
|
|
\i tables/public/community_deletion_requests.sql
|
|
\i tables/public/community_deletion_approvals.sql
|
|
\i tables/public/community_deletion_checkpoints.sql
|
|
\i tables/public/community_deletion_manifest_keys.sql
|
|
\i tables/public/storage_taxonomy_sweeps.sql
|
|
\i tables/public/community_serving_write_leases.sql
|
|
\i tables/public/community_deletion_executor_heartbeats.sql
|
|
\i tables/public/relay_operators.sql
|
|
\i tables/public/relay_admin_actions.sql
|
|
\i tables/public/relay_admin_outbox.sql
|
|
\i tables/public/operator_listener_pubkeys.sql
|
|
\i tables/public/operator_listener_outbox.sql
|
|
\i tables/public/relay_operator_audit.sql
|
|
\i tables/public/storage_accounting_snapshots.sql
|
|
\i tables/public/artifact_heads.sql
|
|
\i tables/public/artifact_revisions.sql
|
|
|
|
-- partitions
|
|
\i partitions/events.sql
|
|
\i partitions/delivery_log.sql
|
|
|
|
-- triggers
|
|
\i triggers/channels.sql
|
|
\i triggers/events.sql
|
|
\i triggers/community_deletion_requests.sql
|
|
\i triggers/community_deletion_approvals.sql
|
|
\i triggers/community_deletion_manifest_keys.sql
|
|
\i triggers/communities.sql
|
|
\i triggers/api_tokens.sql
|
|
\i triggers/archived_identities.sql
|
|
\i triggers/audit_log.sql
|
|
\i triggers/channel_members.sql
|
|
\i triggers/community_bans.sql
|
|
\i triggers/community_nip_fi_issuers.sql
|
|
\i triggers/delivery_log.sql
|
|
\i triggers/event_mentions.sql
|
|
\i triggers/git_repo_names.sql
|
|
\i triggers/join_policy_acceptances.sql
|
|
\i triggers/moderation_actions.sql
|
|
\i triggers/moderation_reports.sql
|
|
\i triggers/parameterized_event_watermarks.sql
|
|
\i triggers/personal_read_accounts.sql
|
|
\i triggers/personal_read_frontiers.sql
|
|
\i triggers/pubkey_allowlist.sql
|
|
\i triggers/push_leases.sql
|
|
\i triggers/push_match_queue.sql
|
|
\i triggers/push_wake_outbox.sql
|
|
\i triggers/reactions.sql
|
|
\i triggers/relay_invites.sql
|
|
\i triggers/relay_members.sql
|
|
\i triggers/scheduled_workflow_fires.sql
|
|
\i triggers/subscriptions.sql
|
|
\i triggers/thread_metadata.sql
|
|
\i triggers/users.sql
|
|
\i triggers/workflow_approvals.sql
|
|
\i triggers/workflow_runs.sql
|
|
\i triggers/workflows.sql
|
|
\i triggers/artifact_heads.sql
|
|
\i triggers/artifact_revisions.sql
|
|
|
|
-- seeds
|
|
\i seeds/communities.sql
|
|
\i seeds/rate_limit_violations.sql
|
|
\i seeds/_operator_global_tables.sql
|
|
\i seeds/product_feedback.sql
|
|
\i seeds/push_gateway_challenges.sql
|
|
\i seeds/push_gateway_installations.sql
|
|
\i seeds/push_gateway_delegations.sql
|
|
\i seeds/push_gateway_endpoint_quotas.sql
|
|
\i seeds/push_gateway_delivery_auth_replays.sql
|
|
\i seeds/push_gateway_delivery_request_replays.sql
|
|
\i seeds/replica_heartbeat.sql
|
|
\i seeds/community_deletion_requests.sql
|
|
\i seeds/community_deletion_approvals.sql
|
|
\i seeds/community_deletion_checkpoints.sql
|
|
\i seeds/community_deletion_manifest_keys.sql
|
|
\i seeds/storage_taxonomy_sweeps.sql
|
|
\i seeds/community_serving_write_leases.sql
|
|
\i seeds/community_deletion_executor_heartbeats.sql
|
|
\i seeds/relay_operators.sql
|
|
\i seeds/relay_admin_actions.sql
|
|
\i seeds/relay_admin_outbox.sql
|
|
\i seeds/operator_listener_pubkeys.sql
|
|
\i seeds/operator_listener_outbox.sql
|
|
\i seeds/relay_operator_audit.sql
|
|
\i seeds/storage_accounting_snapshots.sql
|
|
|
|
-- Fence any community-scoped table that lacks an explicit attach.
|
|
\i community_write_fence.sql
|