mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
## Summary User deletion can now discover current communities from a hex public key or npub, save that inventory, and run it through the existing approval and dummy workflow. `--pubkey demo` remains the default. Execution only prints messages; it does not fence users or delete data. Discovery combines explicit relay memberships with active channel memberships, including users who have no relay membership row. It deduplicates communities and preserves owner relationships. The saved manifest contains only community IDs, labels and relationships. Historical discovery and owned-community execution remain later work. Submission rejects owner inventories atomically with a specific unsupported-execution error; preview still includes owners. This is an expand-schema change after #8520. Migrations 0072–0073 replace the demo-only target check with a bounded identifier check, with 5-second lock and 30-second statement timeouts. The database keeps the user-only entity-type check while allowing different identity formats in the target field. Workflow-specific commands reject entity types they cannot execute. The application currently resolves demo and hex/npub inputs; account-ID lookup is future work. The desired-state constraint matches. No backfill, trigger, function, or foreign key is added. Existing demo rows remain valid. Old binaries fail closed on new manifests; use the new CLI for their approval and execution. Older auto-migrating binaries need the documented rollback procedure. Identity parsing and demo fixture selection stay in intake. Approval and resume use the saved target without reparsing its format. The user workflow defines its stage names and handlers; CLI stage validation reads those rules instead of a separate three-name list. Shared persistence accepts caller-defined stages. Permanent target uniqueness is unchanged. Dummy submissions and execution belong in isolated, disposable development databases; production preview uses read-only access. CLI operator names remain self-asserted. ### Related issue Follows #8520. Searched open PRs; no duplicate inventory change found. #8489 is earlier workflow scaffolding. ### Testing Built `buzz-admin` locally and applied all migrations to a fresh disposable PostgreSQL database. Ran the actual CLI through preview → submit → inspect → approve → `run --next-step` → resume → completed for both demo and a public key with a channel-only membership fixture. Demo completed five stages; the public-key run completed three. Injected `--fail-stage purge_db`, waited for the retry deadline, and resumed the same failed stage successfully. Membership data remained intact. Repeated the CLI workflow on October 26 for demo and an npub backed by a fresh production inventory. Production access used read-only credentials and the real `inventory --pubkey "$SUBJECT"` handler. Copied only the relevant discovery fields into an isolated local PostgreSQL database; local channel parent rows supplied placeholder fields for foreign keys. The inventory saved by local submission exactly matched the production result. All workflow writes stayed local. For each input, ran `submit --pubkey "$SUBJECT" --requested-by local-e2e-submitter`, inspected the saved inventory, and confirmed execution before approval was rejected. Approved its digest, then ran each handler with `run "$REQUEST_ID" --next-step --stage-delay-ms 2500` in a new process. Captured all three workflow tables during and after every stage. Demo completed five stages across two communities; the npub completed three stages for its discovered community. Each handler saved `dummy: true`. Both requests finished with no current stage, no worker lease, and no retries. Copied memberships remained intact. This additional run exercised orderly resume; it did not inject a failure. The reproducible command sequence and fixture setup are in [the inventory walkthrough](docs/operator-user-deletion-inventory.md). Production was not changed. The earlier full-workflow runs used synthetic local fixtures; the additional npub run used captured production discovery fields. Repository-wide `just ci` was not run; validation focused on the affected packages and PostgreSQL workflow. The pre-push desktop build failed with “Too many open files in system.” Stopped the broader hook run and bypassed it to publish the earlier commits; the affected-package Clippy and formatting checks had passed. Generated with Codex --------- Signed-off-by: Ravneet Arora <rarora@squareup.com>
158 lines
5.5 KiB
Rust
158 lines
5.5 KiB
Rust
//! Read-only user inventory and synthetic user-deletion workflow commands.
|
|
|
|
use anyhow::Result;
|
|
use clap::Subcommand;
|
|
use uuid::Uuid;
|
|
|
|
/// Preview real memberships or prepare and execute synthetic deletion requests.
|
|
#[derive(Debug, Subcommand)]
|
|
pub enum Command {
|
|
/// Print real current memberships as JSON; does not save or execute a request.
|
|
Inventory {
|
|
/// Demo fixture (default), hex public key, or npub. Current memberships only.
|
|
#[arg(long, default_value = "demo", value_parser = inventory::parse_subject)]
|
|
pubkey: String,
|
|
},
|
|
/// Save a request. An identical resubmission returns the existing row.
|
|
Submit {
|
|
/// Optional demo request label, for example demo:alice. Conflicts with --pubkey.
|
|
#[arg(long, conflicts_with = "pubkey")]
|
|
target: Option<String>,
|
|
/// Demo fixture (default), hex public key, or npub to discover and save.
|
|
#[arg(long, value_parser = inventory::parse_subject)]
|
|
pubkey: Option<String>,
|
|
/// Submitting operator identity.
|
|
#[arg(long)]
|
|
requested_by: String,
|
|
/// Optional explanation.
|
|
#[arg(long)]
|
|
reason: Option<String>,
|
|
},
|
|
/// Approve the saved inventory; execution initializes the first stage.
|
|
Approve {
|
|
/// Request UUID returned by submit.
|
|
id: Uuid,
|
|
/// Approving operator identity (self-asserted for this demo).
|
|
#[arg(long)]
|
|
approved_by: String,
|
|
/// Inventory digest from inspect; approval must match this exact inventory.
|
|
#[arg(long)]
|
|
inventory_digest: String,
|
|
},
|
|
/// Run dummy stages for an approved request; no real user data is changed.
|
|
Run {
|
|
/// Approved request UUID.
|
|
id: Uuid,
|
|
/// Worker identity; defaults to hostname and process id.
|
|
#[arg(long)]
|
|
executor_id: Option<String>,
|
|
/// Execute one handler, transition to the next stage, and release ownership.
|
|
#[arg(long)]
|
|
next_step: bool,
|
|
/// Inject a retryable failure when this dummy stage is reached.
|
|
#[arg(long, value_parser = workflow::parse_stage)]
|
|
fail_stage: Option<String>,
|
|
/// Delay each dummy action for inspection and shutdown testing (up to 60s).
|
|
#[arg(long, default_value_t = 0, value_parser = clap::value_parser!(u64).range(0..=60_000))]
|
|
stage_delay_ms: u64,
|
|
/// Simulate no agents in this community; may be repeated. Saves a runtime branch result.
|
|
#[arg(long)]
|
|
demo_no_agents_in: Vec<Uuid>,
|
|
},
|
|
/// List synthetic requests, newest first.
|
|
List {
|
|
/// Maximum number of rows.
|
|
#[arg(long, default_value_t = 100, value_parser = clap::value_parser!(u16).range(1..=1000))]
|
|
limit: u16,
|
|
},
|
|
/// Show a saved request by UUID.
|
|
Inspect {
|
|
/// Request UUID returned by submit.
|
|
id: Uuid,
|
|
},
|
|
}
|
|
|
|
/// Run inventory discovery or synthetic workflow commands against PostgreSQL.
|
|
pub async fn run(command: Command) -> Result<i32> {
|
|
let db = if matches!(command, Command::Inventory { .. }) {
|
|
buzz_db::Db::connect_cold_start(
|
|
buzz_db::DbConfig {
|
|
database_url: crate::required_env("DATABASE_URL")?,
|
|
max_connections: 1,
|
|
min_connections: 0,
|
|
default_transaction_read_only: true,
|
|
..buzz_db::DbConfig::default()
|
|
},
|
|
"user_inventory",
|
|
)
|
|
.await?
|
|
} else {
|
|
crate::connect_database().await?
|
|
};
|
|
let store = db.entity_deletion_store();
|
|
match command {
|
|
Command::Inventory { pubkey } => {
|
|
crate::print_json(&inventory::generate(&db, &pubkey).await?)?;
|
|
}
|
|
Command::Submit {
|
|
target,
|
|
pubkey,
|
|
requested_by,
|
|
reason,
|
|
} => {
|
|
let target = target.unwrap_or_else(|| match pubkey.as_deref() {
|
|
None | Some("demo") => "demo:default".to_owned(),
|
|
Some(key) => key.to_owned(),
|
|
});
|
|
crate::print_json(
|
|
&db.user_deletion_store()
|
|
.submit(&target, &requested_by, reason.as_deref())
|
|
.await?,
|
|
)?;
|
|
}
|
|
Command::Approve {
|
|
id,
|
|
approved_by,
|
|
inventory_digest,
|
|
} => {
|
|
db.user_deletion_store()
|
|
.approve(id, &approved_by, &inventory_digest)
|
|
.await?;
|
|
crate::print_json(&store.inspect(id).await?)?;
|
|
}
|
|
Command::Run {
|
|
id,
|
|
executor_id,
|
|
next_step,
|
|
fail_stage,
|
|
stage_delay_ms,
|
|
demo_no_agents_in,
|
|
} => {
|
|
let code = worker::run(
|
|
&store,
|
|
id,
|
|
&executor_id.unwrap_or_else(crate::default_executor_id),
|
|
&worker::RunOptions {
|
|
next_step,
|
|
fail_stage,
|
|
stage_delay: std::time::Duration::from_millis(stage_delay_ms),
|
|
no_agents_in: demo_no_agents_in,
|
|
},
|
|
&crate::shutdown_token(),
|
|
)
|
|
.await?;
|
|
crate::print_json(&store.inspect(id).await?)?;
|
|
return Ok(code);
|
|
}
|
|
Command::List { limit } => crate::print_json(&store.list(limit).await?)?,
|
|
Command::Inspect { id } => crate::print_json(&store.inspect(id).await?)?,
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
mod handlers;
|
|
mod inventory;
|
|
mod transition;
|
|
mod worker;
|
|
mod workflow;
|