Skip to content

Commit 004d9fd

Browse files
committed
fix(node): filter global anchors before limit and use deduped repo visibility
1 parent 128d633 commit 004d9fd

3 files changed

Lines changed: 68 additions & 38 deletions

File tree

‎crates/gitlawb-node/src/api/arweave.rs‎

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use serde::Deserialize;
99
use crate::auth::AuthenticatedDid;
1010
use crate::error::{AppError, Result};
1111
use crate::state::AppState;
12+
use crate::visibility::{visibility_check, Decision};
1213

1314
#[derive(Debug, Deserialize)]
1415
pub struct ListAnchorsQuery {
@@ -67,33 +68,54 @@ pub async fn list_anchors(
6768
};
6869

6970
let limit = q.limit.clamp(0, 200);
70-
let raw_anchors = state
71-
.db
72-
.list_arweave_anchors(normalized_repo.as_deref(), limit)
73-
.await
74-
.map_err(AppError::Internal)?;
7571

76-
// For global listings (no ?repo=), filter each anchor on current visibility.
77-
let anchors = if normalized_repo.is_none() {
78-
let mut filtered = Vec::new();
79-
for anchor in raw_anchors {
80-
// Parse repo slug to resolve current visibility.
81-
let parts: Vec<&str> = anchor.repo.splitn(2, '/').collect();
82-
if parts.len() != 2 {
83-
continue;
84-
}
85-
let (owner, name) = (parts[0], parts[1]);
86-
// Skip anchors for repos the caller cannot currently read.
87-
if crate::api::authorize_repo_read(&state, owner, name, caller, "/")
88-
.await
89-
.is_ok()
90-
{
91-
filtered.push(anchor);
92-
}
93-
}
94-
filtered
72+
let anchors = if let Some(ref slug) = normalized_repo {
73+
// Per-repo: filter at the DB level and return directly.
74+
state
75+
.db
76+
.list_arweave_anchors(Some(slug), limit)
77+
.await
78+
.map_err(AppError::Internal)?
9579
} else {
96-
raw_anchors
80+
// Global listing: fetch all anchors, filter by current visibility
81+
// against the deduped repo view (so mirror rows never bypass canonical
82+
// visibility), then take limit.
83+
let all_anchors = state
84+
.db
85+
.list_arweave_anchors(None, i64::MAX)
86+
.await
87+
.map_err(AppError::Internal)?;
88+
89+
// Build a set of readable repo short-form slugs from the deduped list.
90+
let repos = state
91+
.db
92+
.list_all_repos_deduped()
93+
.await
94+
.map_err(AppError::Internal)?;
95+
let repo_ids: Vec<String> = repos.iter().map(|r| r.id.clone()).collect();
96+
let rules_by_repo = state
97+
.db
98+
.list_visibility_rules_for_repos(&repo_ids)
99+
.await
100+
.map_err(AppError::Internal)?;
101+
102+
let readable: std::collections::HashSet<String> = repos
103+
.iter()
104+
.filter(|r| {
105+
let rules = rules_by_repo.get(&r.id).map(Vec::as_slice).unwrap_or(&[]);
106+
visibility_check(rules, r.is_public, &r.owner_did, caller, "/") != Decision::Deny
107+
})
108+
.map(|r| {
109+
let short = r.owner_did.split(':').next_back().unwrap_or(&r.owner_did);
110+
format!("{}/{}", short, r.name)
111+
})
112+
.collect();
113+
114+
all_anchors
115+
.into_iter()
116+
.filter(|a| readable.contains(&a.repo))
117+
.take(limit as usize)
118+
.collect()
97119
};
98120

99121
Ok(Json(serde_json::json!({

‎crates/gitlawb-node/src/api/ipfs.rs‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,11 @@ pub async fn list_pins(
246246
.map_err(AppError::Internal)?;
247247

248248
// Build a set of sha256_hex values from repos the caller can read.
249+
// Use the deduped repo list so mirror rows never bypass the canonical
250+
// repo's visibility rules (#136).
249251
let repos = state
250252
.db
251-
.list_all_repos()
253+
.list_all_repos_deduped()
252254
.await
253255
.map_err(AppError::Internal)?;
254256

@@ -262,18 +264,6 @@ pub async fn list_pins(
262264
let mut allowed_sha256s = std::collections::HashSet::new();
263265

264266
for repo in &repos {
265-
// Preserve the quarantine gate from authorize_repo_read: a quarantined
266-
// mirror is treated as nonexistent on every read surface, so its objects
267-
// must not contribute to the allowed SHA-256 set (#P2).
268-
if state
269-
.db
270-
.is_repo_quarantined(&repo.id)
271-
.await
272-
.map_err(AppError::Internal)?
273-
{
274-
continue;
275-
}
276-
277267
let rules: &[crate::db::VisibilityRule] = rules_by_repo
278268
.get(&repo.id)
279269
.map(Vec::as_slice)

‎crates/gitlawb-node/src/test_support.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,4 +2372,22 @@ mod tests {
23722372
.unwrap();
23732373
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
23742374
}
2375+
2376+
/// #121: negative limit is clamped to 0 and returns a bounded response (no 500).
2377+
#[sqlx::test]
2378+
async fn anchors_global_negative_limit_is_clamped(pool: PgPool) {
2379+
let state = test_state(pool).await;
2380+
let fx = setup_anchor_test(&state, "neg-limit-repo", true).await;
2381+
2382+
let resp = anchors_router(&state)
2383+
.oneshot(signed_get(&fx.owner, "/api/v1/arweave/anchors?limit=-1"))
2384+
.await
2385+
.unwrap();
2386+
assert_eq!(resp.status(), StatusCode::OK);
2387+
let body = json_body(resp).await;
2388+
assert_eq!(
2389+
body["count"], 0,
2390+
"negative limit clamps to 0, returning empty result"
2391+
);
2392+
}
23752393
}

0 commit comments

Comments
 (0)