Skip to content

Commit eef1781

Browse files
lmvasquezgmeta-codesync[bot]
authored andcommitted
Add per-repo manifest-id lookup and delete to restricted paths store
Summary: The ACL Manifest Store records a manifest id per restricted directory so that access can be checked by manifest id without resolving the full path. Cleaning up content-hash collisions in this store (trivial directories that hash to the same manifest id) requires looking up and removing entries by manifest id regardless of manifest type, which the existing per-manifest-type methods do not support. This adds `get_all_paths_by_manifest_id` and `delete_by_manifest_id` to the restricted paths manifest-id store, backed by two new `mononoke_queries!` entries that match on `repo_id` and `manifest_id` across all manifest types. Both operations are scoped to the store's repo, consistent with the rest of the trait. A unit test exercises the per-repo select and delete behavior against a shared in-memory sqlite DB, verifying that entries in other repos and entries with a different manifest id are left untouched. The admin command that uses these methods to clean up collisions is added in the commit on top of this one (D110107711). Reviewed By: gustavoavena Differential Revision: D110105804 fbshipit-source-id: 7daa7179dff1dfe56d28901ae470277887962a1a
1 parent b6f28f6 commit eef1781

3 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎eden/mononoke/repo_attributes/restricted_paths_common/BUCK‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ rust_library(
88
"src/**/*.rs",
99
"schemas/**/*.sql",
1010
]),
11+
test_deps = [
12+
"//common/rust/shed/fbinit:fbinit",
13+
"//common/rust/shed/fbinit:fbinit-tokio",
14+
],
1115
deps = [
1216
"fbsource//third-party/rust:anyhow",
1317
"fbsource//third-party/rust:async-trait",

‎eden/mononoke/repo_attributes/restricted_paths_common/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,7 @@ sql_ext = { version = "0.1.0", path = "../../common/rust/sql_ext" }
2727
strum = { version = "0.28.0", features = ["derive"] }
2828
tokio = { version = "1.52.3", features = ["full", "test-util", "tracing"] }
2929
tracing = { version = "0.1.41", features = ["attributes", "valuable"] }
30+
31+
[dev-dependencies]
32+
fbinit = { version = "0.2.0", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" }
33+
fbinit-tokio = { version = "0.1.2", git = "https://github.com/facebookexperimental/rust-shed.git", branch = "main" }

‎eden/mononoke/repo_attributes/restricted_paths_common/src/manifest_id_store.rs‎

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,22 @@ pub trait RestrictedPathsManifestIdStore: Send + Sync {
140140
// TODO(T239041722): add limit
141141
) -> Result<Vec<RestrictedPathManifestIdEntry>>;
142142

143+
/// Get all entries in this repo matching a manifest id, regardless of
144+
/// manifest type. Returns the manifest type and path of each matching entry.
145+
async fn get_all_paths_by_manifest_id(
146+
&self,
147+
ctx: &CoreContext,
148+
manifest_id: &ManifestId,
149+
) -> Result<Vec<(ManifestType, NonRootMPath)>>;
150+
151+
/// Delete all entries in this repo matching a manifest id, regardless of
152+
/// manifest type. Returns the number of rows deleted.
153+
async fn delete_by_manifest_id(
154+
&self,
155+
ctx: &CoreContext,
156+
manifest_id: &ManifestId,
157+
) -> Result<u64>;
158+
143159
fn repo_id(&self) -> RepositoryId;
144160
}
145161

@@ -186,6 +202,18 @@ mononoke_queries! {
186202
"
187203
}
188204

205+
read SelectByManifestId(
206+
repo_id: RepositoryId,
207+
manifest_id: ManifestId,
208+
) -> (ManifestType, NonRootMPath) {
209+
"SELECT manifest_type, path FROM restricted_paths_manifest_ids WHERE repo_id = {repo_id} AND manifest_id = {manifest_id}"
210+
}
211+
212+
write DeleteByManifestId(repo_id: RepositoryId, manifest_id: ManifestId) {
213+
none,
214+
"DELETE FROM restricted_paths_manifest_ids WHERE repo_id = {repo_id} AND manifest_id = {manifest_id}"
215+
}
216+
189217
}
190218

191219
pub struct SqlRestrictedPathsManifestIdStore {
@@ -278,6 +306,38 @@ impl RestrictedPathsManifestIdStore for SqlRestrictedPathsManifestIdStore {
278306
.collect::<Result<_>>()
279307
}
280308

309+
async fn get_all_paths_by_manifest_id(
310+
&self,
311+
ctx: &CoreContext,
312+
manifest_id: &ManifestId,
313+
) -> Result<Vec<(ManifestType, NonRootMPath)>> {
314+
let rows = SelectByManifestId::query(
315+
&self.connections.read_connection,
316+
ctx.sql_query_telemetry(),
317+
&self.repo_id,
318+
manifest_id,
319+
)
320+
.await?;
321+
322+
Ok(rows)
323+
}
324+
325+
async fn delete_by_manifest_id(
326+
&self,
327+
ctx: &CoreContext,
328+
manifest_id: &ManifestId,
329+
) -> Result<u64> {
330+
let result = DeleteByManifestId::query(
331+
&self.connections.write_connection,
332+
ctx.sql_query_telemetry(),
333+
&self.repo_id,
334+
manifest_id,
335+
)
336+
.await?;
337+
338+
Ok(result.affected_rows())
339+
}
340+
281341
fn repo_id(&self) -> RepositoryId {
282342
self.repo_id
283343
}
@@ -467,3 +527,119 @@ fn fmt_path_bytes(path: &PathBytes, f: &mut fmt::Formatter) -> fmt::Result {
467527
fn fmt_path_hash_bytes(path_hash: &PathHashBytes, f: &mut fmt::Formatter) -> fmt::Result {
468528
write!(f, "\"{}\"", hex::encode(&path_hash.0))
469529
}
530+
531+
#[cfg(test)]
532+
mod tests {
533+
use fbinit::FacebookInit;
534+
use mononoke_macros::mononoke;
535+
536+
use super::*;
537+
538+
fn manifest_id_from(bytes: &[u8]) -> ManifestId {
539+
ManifestId::new(SmallVec::from_slice(bytes))
540+
}
541+
542+
fn entry(
543+
manifest_type: ManifestType,
544+
manifest_id: &ManifestId,
545+
path: &str,
546+
) -> RestrictedPathManifestIdEntry {
547+
RestrictedPathManifestIdEntry::new(
548+
manifest_type,
549+
manifest_id.clone(),
550+
RepoPath::dir(NonRootMPath::new(path).expect("valid path"))
551+
.expect("valid directory repo path"),
552+
)
553+
.expect("valid entry")
554+
}
555+
556+
#[mononoke::fbinit_test]
557+
async fn test_get_and_delete_by_manifest_id_per_repo(fb: FacebookInit) -> Result<()> {
558+
let ctx = CoreContext::test_mock(fb);
559+
560+
// Build the first store via the builder so it owns an in-memory sqlite
561+
// DB, then build a second store for a different repo that shares the
562+
// SAME connections so both repos hit the same underlying DB. This lets
563+
// us assert that the per-repo operations only touch their own repo.
564+
let builder = SqlRestrictedPathsManifestIdStoreBuilder::with_sqlite_in_memory()?;
565+
let connections = builder.connections.clone();
566+
let store_repo1 = builder.with_repo_id(RepositoryId::new(1));
567+
let store_repo2 = SqlRestrictedPathsManifestIdStore::new(RepositoryId::new(2), connections);
568+
569+
let shared_id = manifest_id_from(&[1u8; 32]);
570+
let control_id = manifest_id_from(&[2u8; 32]);
571+
572+
// The same manifest id is stored in both repos under two manifest types.
573+
store_repo1
574+
.add_entry(&ctx, entry(ManifestType::Hg, &shared_id, "repo1/hg"))
575+
.await?;
576+
store_repo1
577+
.add_entry(
578+
&ctx,
579+
entry(ManifestType::HgAugmented, &shared_id, "repo1/hg_aug"),
580+
)
581+
.await?;
582+
store_repo2
583+
.add_entry(&ctx, entry(ManifestType::Hg, &shared_id, "repo2/hg"))
584+
.await?;
585+
store_repo2
586+
.add_entry(
587+
&ctx,
588+
entry(ManifestType::HgAugmented, &shared_id, "repo2/hg_aug"),
589+
)
590+
.await?;
591+
592+
// A control entry with a different manifest id must survive deletion.
593+
store_repo1
594+
.add_entry(&ctx, entry(ManifestType::Hg, &control_id, "repo1/control"))
595+
.await?;
596+
597+
// (a) get_all_paths_by_manifest_id is scoped to the store's repo: repo1
598+
// sees only its own two entries, not repo2's.
599+
let found = store_repo1
600+
.get_all_paths_by_manifest_id(&ctx, &shared_id)
601+
.await?;
602+
assert_eq!(
603+
found.len(),
604+
2,
605+
"repo1 should only see its own two entries for the shared manifest id"
606+
);
607+
608+
// (b) delete_by_manifest_id removes only repo1's two entries.
609+
let deleted = store_repo1.delete_by_manifest_id(&ctx, &shared_id).await?;
610+
assert_eq!(deleted, 2, "delete should remove only repo1's two entries");
611+
assert!(
612+
store_repo1
613+
.get_all_paths_by_manifest_id(&ctx, &shared_id)
614+
.await?
615+
.is_empty(),
616+
"no repo1 shared-id entries should remain after deletion"
617+
);
618+
619+
// (c) repo2's entries for the same manifest id are untouched.
620+
assert_eq!(
621+
store_repo2
622+
.get_all_paths_by_manifest_id(&ctx, &shared_id)
623+
.await?
624+
.len(),
625+
2,
626+
"repo2's entries must not be affected by deleting from repo1"
627+
);
628+
629+
// (d) a second delete is a no-op.
630+
let deleted_again = store_repo1.delete_by_manifest_id(&ctx, &shared_id).await?;
631+
assert_eq!(deleted_again, 0, "second delete should affect no rows");
632+
633+
// (e) the control entry with a different id is untouched.
634+
assert_eq!(
635+
store_repo1
636+
.get_all_paths_by_manifest_id(&ctx, &control_id)
637+
.await?
638+
.len(),
639+
1,
640+
"the control entry with a different manifest id should remain"
641+
);
642+
643+
Ok(())
644+
}
645+
}

0 commit comments

Comments
 (0)