mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(acp): add BUZZ_ACP_ALLOWED_RESPOND_TO and BUZZ_ALLOWED_CHANNEL_ADD_POLICIES gates (#1304)
Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
co-authored by
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent
6b0564618b
commit
1a61d783ad
@@ -527,6 +527,26 @@ pub async fn cmd_set_add_policy(client: &BuzzClient, policy: &str) -> Result<(),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this policy is allowed by the deployment.
|
||||
// NOTE: This gate covers only the `buzz channels set-add-policy` CLI path.
|
||||
// A client that submits a kind:10100 event directly to the relay bypasses
|
||||
// this check. Full enforcement requires relay-side validation, which is
|
||||
// intentionally out of scope for this change (see team decision: no
|
||||
// relay-side enforcement of client behavior).
|
||||
if let Ok(allowed_raw) = std::env::var("BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES") {
|
||||
let allowed: Vec<&str> = allowed_raw
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !allowed.is_empty() && !allowed.contains(&policy) {
|
||||
return Err(CliError::Usage(format!(
|
||||
"channel_add_policy '{policy}' is not permitted on this deployment \
|
||||
(BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES={allowed_raw})"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let content = serde_json::json!({ "channel_add_policy": policy }).to_string();
|
||||
use nostr::{EventBuilder, Kind};
|
||||
let builder = EventBuilder::new(
|
||||
@@ -648,7 +668,9 @@ pub async fn dispatch_canvas(cmd: crate::CanvasCmd, client: &BuzzClient) -> Resu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{name_matches, validate_ttl_seconds, ChannelSummary};
|
||||
use super::{cmd_set_add_policy, name_matches, validate_ttl_seconds, ChannelSummary};
|
||||
use crate::client::BuzzClient;
|
||||
use crate::CliError;
|
||||
use serde_json::json;
|
||||
|
||||
fn event(tags: serde_json::Value) -> serde_json::Value {
|
||||
@@ -756,4 +778,93 @@ mod tests {
|
||||
fn validate_ttl_rejects_overflow() {
|
||||
assert!(validate_ttl_seconds(i32::MAX as i64 + 1).is_err());
|
||||
}
|
||||
|
||||
// --- BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES gate ---
|
||||
|
||||
fn check_allowed_channel_add_policy(allowed_raw: &str, policy: &str) -> Result<(), CliError> {
|
||||
let allowed: Vec<&str> = allowed_raw
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
if !allowed.is_empty() && !allowed.contains(&policy) {
|
||||
return Err(CliError::Usage(format!(
|
||||
"channel_add_policy '{policy}' is not permitted on this deployment \
|
||||
(BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES={allowed_raw})"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_add_policy_rejects_disallowed_policy() {
|
||||
let result = check_allowed_channel_add_policy("owner_only,nobody", "anyone");
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"anyone should be rejected when not in allowed set"
|
||||
);
|
||||
let msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
msg.contains("not permitted"),
|
||||
"error should mention 'not permitted': {msg}"
|
||||
);
|
||||
assert!(
|
||||
msg.contains("anyone"),
|
||||
"error should name the disallowed policy: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_add_policy_accepts_allowed_policy() {
|
||||
let result = check_allowed_channel_add_policy("owner_only,nobody", "owner_only");
|
||||
assert!(result.is_ok(), "owner_only should be accepted: {result:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_add_policy_no_restriction_allows_all() {
|
||||
// Empty allowed list means no restriction.
|
||||
let result = check_allowed_channel_add_policy("", "anyone");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"empty allowed list should permit any policy: {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Integration test: full env-var → cmd_set_add_policy() path ---
|
||||
//
|
||||
// This test calls cmd_set_add_policy directly with the env var set. The function
|
||||
// returns early with an error before any network call, so no relay is needed.
|
||||
// If the BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES check were removed from cmd_set_add_policy,
|
||||
// this test would fail (it would proceed to sign_event and return a different error).
|
||||
|
||||
fn make_test_client() -> BuzzClient {
|
||||
// Scalar = 1 is the smallest valid secp256k1 private key.
|
||||
let keys =
|
||||
nostr::Keys::parse("0000000000000000000000000000000000000000000000000000000000000001")
|
||||
.expect("valid test key");
|
||||
BuzzClient::new("ws://localhost:3000".to_string(), keys, None, None)
|
||||
.expect("client construction should not fail")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn set_add_policy_env_gate_rejects_disallowed_via_full_path() {
|
||||
std::env::set_var("BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES", "owner_only,nobody");
|
||||
let client = make_test_client();
|
||||
let result = cmd_set_add_policy(&client, "anyone").await;
|
||||
std::env::remove_var("BUZZ_ACP_ALLOWED_CHANNEL_ADD_POLICIES");
|
||||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"cmd_set_add_policy should reject 'anyone' when not in allowed set"
|
||||
);
|
||||
match result.unwrap_err() {
|
||||
crate::CliError::Usage(msg) => {
|
||||
assert!(
|
||||
msg.contains("not permitted"),
|
||||
"error should mention 'not permitted': {msg}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected CliError::Usage, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user