mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
fix(grasp06): reject /prs/ push when event lacks clone tag naming this relay
The last refactor introduced PrsUrlConstraints to share pre-validation between the standard and /prs/ endpoints, but omitted the clone-tag check from describe_known_event_mismatch. This meant a push to /prs/<author>/<id>.git for an event already in the DB would be accepted even if the event's clone tag did not name this relay's /prs/ endpoint — defeating the opt-in guard that prevents every GRASP-06 relay from becoming an unsolicited mirror for every PR event on the network. Fix: add domain to PrsUrlConstraints and check the event's clone tag in describe_known_event_mismatch using the existing clone_url_names_relays_prs_endpoint helper (made pub). Thread domain through handle_prs_receive_pack and post_push_validate.
This commit is contained in:
@@ -1163,9 +1163,13 @@ pub fn extract_commit_tag(event: &Event) -> Option<String> {
|
||||
///
|
||||
/// When present, a known event found in the DB or purgatory MUST have:
|
||||
///
|
||||
/// - `event.pubkey == submitter`, and
|
||||
/// - `event.pubkey == submitter`,
|
||||
/// - at least one `a`-tag of the form `30617:<hex>:<d>` where `d ==
|
||||
/// identifier`.
|
||||
/// identifier`, AND
|
||||
/// - at least one `clone` tag naming this relay's
|
||||
/// `/prs/<submitter-npub>/<identifier>.git` endpoint (the opt-in
|
||||
/// signal that prevents every GRASP-06 relay from accepting every PR
|
||||
/// event that happens to match its URL shape).
|
||||
///
|
||||
/// Standard `/<npub>/<id>.git` pushes pass `None` here — they rely on the
|
||||
/// surrounding `authorize_push` flow to gate by the maintainer set
|
||||
@@ -1174,6 +1178,8 @@ pub fn extract_commit_tag(event: &Event) -> Option<String> {
|
||||
pub struct PrsUrlConstraints<'a> {
|
||||
pub submitter: &'a PublicKey,
|
||||
pub identifier: &'a str,
|
||||
/// The relay's own domain (host[:port]) used to verify the `clone` tag.
|
||||
pub domain: &'a str,
|
||||
}
|
||||
|
||||
/// Outcome of [`pre_validate_refs_nostr_push`] for one ref.
|
||||
@@ -1360,6 +1366,35 @@ fn describe_known_event_mismatch(
|
||||
}
|
||||
None => return Some("has no parsable a-tag identifier".to_string()),
|
||||
}
|
||||
// The event must explicitly opt in to this relay's /prs/ endpoint via
|
||||
// a `clone` tag. Without this check any PR event whose signer and
|
||||
// identifier happen to match the URL could be pushed here, turning
|
||||
// every GRASP-06 relay into an unsolicited mirror for every PR event
|
||||
// on the network.
|
||||
let d_tags = vec![prs.identifier.to_string()];
|
||||
let has_clone_tag = event.tags.iter().any(|tag| {
|
||||
let parts = tag.clone().to_vec();
|
||||
if parts.first().map(String::as_str) != Some("clone") {
|
||||
return false;
|
||||
}
|
||||
parts.iter().skip(1).any(|url| {
|
||||
crate::grasp06::policy::clone_url_names_relays_prs_endpoint(
|
||||
url,
|
||||
prs.domain,
|
||||
prs.submitter,
|
||||
&d_tags,
|
||||
)
|
||||
})
|
||||
});
|
||||
if !has_clone_tag {
|
||||
return Some(format!(
|
||||
"has no `clone` tag naming this relay's /prs/{}/{}.git endpoint",
|
||||
prs.submitter
|
||||
.to_bech32()
|
||||
.unwrap_or_else(|_| prs.submitter.to_hex()),
|
||||
prs.identifier,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
|
||||
@@ -112,7 +112,7 @@ fn collect_a_tag_d_values(event: &Event) -> Vec<String> {
|
||||
/// - `<npub-segment>` decodes via [`PublicKey::from_bech32`] to `signer`,
|
||||
/// - the percent-decoded part of `<repo-segment>` before `.git` matches one
|
||||
/// of `d_tags`.
|
||||
fn clone_url_names_relays_prs_endpoint(
|
||||
pub fn clone_url_names_relays_prs_endpoint(
|
||||
url: &str,
|
||||
domain: &str,
|
||||
signer: &PublicKey,
|
||||
|
||||
+11
-9
@@ -152,6 +152,7 @@ pub async fn handle_prs_receive_pack(
|
||||
git_data_path: &str,
|
||||
git_protocol: Option<&str>,
|
||||
repo_init_locks: RepoInitLocks,
|
||||
domain: &str,
|
||||
) -> Result<Response<Full<Bytes>>, GitError> {
|
||||
// 1. Pre-scan refs and reject the whole push if any ref name is not
|
||||
// `refs/nostr/<64-lowercase-hex>`. We use the same parser as the
|
||||
@@ -196,6 +197,7 @@ pub async fn handle_prs_receive_pack(
|
||||
let prs_constraints = PrsUrlConstraints {
|
||||
submitter: &prs.submitter,
|
||||
identifier: &prs.identifier,
|
||||
domain,
|
||||
};
|
||||
for (_, new_oid, ref_name) in &pushed_refs {
|
||||
match pre_validate_refs_nostr_push(
|
||||
@@ -274,6 +276,11 @@ pub async fn handle_prs_receive_pack(
|
||||
// event yet — and creates a scoped placeholder so the
|
||||
// 30-minute purgatory sweep can clean it up if the event
|
||||
// never arrives.
|
||||
let post_push_constraints = PrsUrlConstraints {
|
||||
submitter: &prs.submitter,
|
||||
identifier: &prs.identifier,
|
||||
domain,
|
||||
};
|
||||
for (_, new_oid, ref_name) in &pushed_refs {
|
||||
let event_id_hex = ref_name
|
||||
.strip_prefix("refs/nostr/")
|
||||
@@ -282,7 +289,7 @@ pub async fn handle_prs_receive_pack(
|
||||
&database,
|
||||
&purgatory,
|
||||
&repo_path,
|
||||
prs,
|
||||
post_push_constraints,
|
||||
event_id_hex,
|
||||
new_oid,
|
||||
ref_name,
|
||||
@@ -518,16 +525,11 @@ async fn post_push_validate(
|
||||
database: &SharedDatabase,
|
||||
purgatory: &Purgatory,
|
||||
repo_path: &Path,
|
||||
prs: &PrsUrl,
|
||||
prs_constraints: PrsUrlConstraints<'_>,
|
||||
event_id_hex: &str,
|
||||
pushed_commit: &str,
|
||||
ref_name: &str,
|
||||
) {
|
||||
let prs_constraints = PrsUrlConstraints {
|
||||
submitter: &prs.submitter,
|
||||
identifier: &prs.identifier,
|
||||
};
|
||||
|
||||
match pre_validate_refs_nostr_push(
|
||||
database,
|
||||
purgatory,
|
||||
@@ -567,8 +569,8 @@ async fn post_push_validate(
|
||||
purgatory.add_prs_pr_placeholder(
|
||||
event_id_hex.to_string(),
|
||||
pushed_commit.to_string(),
|
||||
prs.submitter,
|
||||
prs.identifier.clone(),
|
||||
*prs_constraints.submitter,
|
||||
prs_constraints.identifier.to_string(),
|
||||
);
|
||||
debug!(
|
||||
"/prs/ post-push: added scoped PR placeholder for {} awaiting matching event",
|
||||
|
||||
@@ -211,6 +211,7 @@ impl Service<Request<Incoming>> for HttpService {
|
||||
let method_clone = method.clone();
|
||||
let metrics_clone = self.metrics.clone();
|
||||
let relay_clone = self.relay.clone();
|
||||
let config_clone = self.config.clone();
|
||||
|
||||
return Box::pin(async move {
|
||||
// Collect (and gunzip if needed) the request body just like
|
||||
@@ -300,6 +301,7 @@ impl Service<Request<Incoming>> for HttpService {
|
||||
&git_data_path,
|
||||
git_protocol.as_deref(),
|
||||
repo_init_locks.clone(),
|
||||
&config_clone.domain,
|
||||
)
|
||||
.await;
|
||||
if let Some(ref m) = metrics_clone {
|
||||
|
||||
Reference in New Issue
Block a user