mirror of
https://github.com/block/buzz.git
synced 2026-10-31 22:41:12 +00:00
fix(projects): validate reused repo binding
Co-authored-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz> Signed-off-by: Wrench <0eabe6ea5758c1e4c5b68cea4ac42b32c479072883cb28da8110b4a47c32b9a1@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -78,7 +78,7 @@ fn authoritative_channel_repos(
|
||||
if id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let maintainers = tag_values(event, "maintainers")
|
||||
let maintainers = multi_tag_values(event, "maintainers")
|
||||
.map(str::to_ascii_lowercase)
|
||||
.collect();
|
||||
Some(((owner, id.to_string()), maintainers))
|
||||
@@ -101,6 +101,17 @@ fn tag_values<'a>(event: &'a Value, name: &'static str) -> impl Iterator<Item =
|
||||
.filter_map(|tag| tag.get(1).and_then(Value::as_str))
|
||||
}
|
||||
|
||||
fn multi_tag_values<'a>(event: &'a Value, name: &'static str) -> impl Iterator<Item = &'a str> {
|
||||
event
|
||||
.get("tags")
|
||||
.and_then(Value::as_array)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_array)
|
||||
.filter(move |tag| tag.first().and_then(Value::as_str) == Some(name))
|
||||
.flat_map(|tag| tag.iter().skip(1).filter_map(Value::as_str))
|
||||
}
|
||||
|
||||
fn event_has_tag_value(event: &Value, name: &'static str, value: &str) -> bool {
|
||||
tag_values(event, name).any(|candidate| candidate == value)
|
||||
}
|
||||
@@ -210,7 +221,7 @@ mod tests {
|
||||
&owner,
|
||||
"game",
|
||||
CHANNEL_ID,
|
||||
vec![json!(["maintainers", maintainer])],
|
||||
vec![json!(["maintainers", "c".repeat(64), maintainer])],
|
||||
)],
|
||||
CHANNEL_ID,
|
||||
)
|
||||
|
||||
@@ -86,8 +86,10 @@ fn repo_authorizes_project(repo: &Event, project: &Event) -> bool {
|
||||
let signer = project.pubkey.to_hex();
|
||||
repo.pubkey.to_hex().eq_ignore_ascii_case(&signer)
|
||||
|| repo.tags.iter().any(|tag| {
|
||||
matches!(tag.as_slice(), [name, value, ..]
|
||||
if name == "maintainers" && value.eq_ignore_ascii_case(&signer))
|
||||
tag.as_slice().first().map(String::as_str) == Some("maintainers")
|
||||
&& tag.as_slice()[1..]
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(&signer))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -166,6 +168,20 @@ async fn fetch_channel_repos(client: &BuzzClient, channel: &str) -> Result<Vec<E
|
||||
.map_err(|error| CliError::Other(format!("failed to parse relay response: {error}")))
|
||||
}
|
||||
|
||||
fn require_repo_channel_binding(event: &Event, channel: &str) -> Result<(), CliError> {
|
||||
match first_tag_value(event, "buzz-channel") {
|
||||
Some(bound) if bound == channel => Ok(()),
|
||||
Some(bound) => Err(CliError::Conflict(format!(
|
||||
"repository {:?} is already bound to channel {bound}; pass --repo-owner and --repo-id",
|
||||
first_tag_value(event, "d").unwrap_or("<unknown>")
|
||||
))),
|
||||
None => Err(CliError::Conflict(format!(
|
||||
"repository {:?} has no channel binding; bind it to {channel} or pass --repo-owner and --repo-id",
|
||||
first_tag_value(event, "d").unwrap_or("<unknown>")
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_default_repo(
|
||||
client: &BuzzClient,
|
||||
channel: &str,
|
||||
@@ -181,6 +197,7 @@ async fn ensure_default_repo(
|
||||
if let Some(existing) =
|
||||
crate::commands::repos::fetch_own_repo_announcement(client, &repo_id).await?
|
||||
{
|
||||
require_repo_channel_binding(&existing, channel)?;
|
||||
let _ = try_add_own_repo_to_channel_project(client, channel, &repo_id).await;
|
||||
return Ok(ChannelProjectRepo {
|
||||
repo_owner: existing.pubkey.to_hex(),
|
||||
@@ -350,6 +367,61 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_repo_must_bind_requested_channel() {
|
||||
let owner = nostr::Keys::generate();
|
||||
let requested = "11111111-1111-4111-8111-111111111111";
|
||||
let other = "22222222-2222-4222-8222-222222222222";
|
||||
let matching = signed_event(
|
||||
&owner,
|
||||
30617,
|
||||
vec![tag(&["d", "game"]), tag(&["buzz-channel", requested])],
|
||||
);
|
||||
assert!(require_repo_channel_binding(&matching, requested).is_ok());
|
||||
|
||||
let foreign = signed_event(
|
||||
&owner,
|
||||
30617,
|
||||
vec![tag(&["d", "game"]), tag(&["buzz-channel", other])],
|
||||
);
|
||||
assert!(matches!(
|
||||
require_repo_channel_binding(&foreign, requested),
|
||||
Err(CliError::Conflict(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_maintainer_value_authorizes_project() {
|
||||
let owner = nostr::Keys::generate();
|
||||
let maintainer = nostr::Keys::generate();
|
||||
let unrelated = nostr::Keys::generate().public_key().to_hex();
|
||||
let channel = "11111111-1111-4111-8111-111111111111";
|
||||
let owner_hex = owner.public_key().to_hex();
|
||||
let maintainer_hex = maintainer.public_key().to_hex();
|
||||
let repo_coord = format!("30617:{owner_hex}:game");
|
||||
let repo = signed_event(
|
||||
&owner,
|
||||
30617,
|
||||
vec![
|
||||
tag(&["d", "game"]),
|
||||
tag(&["buzz-channel", channel]),
|
||||
tag(&["maintainers", &unrelated, &maintainer_hex]),
|
||||
],
|
||||
);
|
||||
let project = signed_event(
|
||||
&maintainer,
|
||||
30621,
|
||||
vec![
|
||||
tag(&["d", "suite"]),
|
||||
tag(&["buzz-channel", channel]),
|
||||
tag(&["a", &repo_coord]),
|
||||
],
|
||||
);
|
||||
assert!(pick_authoritative_project(&[project], &[repo], channel)
|
||||
.unwrap()
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_repo_a_tag_reads_nip34_coordinate() {
|
||||
let owner = "a".repeat(64);
|
||||
|
||||
Reference in New Issue
Block a user