Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions crates/crab-auth-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ ref updates, staged Xet/Git objects, manifests, indexes, receipts, and
active-active options. Its `Prepare`, `Verify`, and `Commit` commands make the
publish boundary explicit; commit requires the verified plan digest.

Capsule receive captures source and filtered Git/catalog views separately.
Before source materialization, scoped candidates are verified using only the
filtered view and incoming packs. Newly reachable Git objects must be visible
under that capture or supplied by the candidate; retained physical objects do
not grant access. Both receive paths share the graph/visibility verifier.
Receive then queries pointer files and candidate shard/xorb identities under
each capture and applies the candidate delta.
File-only deltas can reference historical filtered artifacts; source-first
file selection and immutable descriptor conflicts remain enforced. Prepare
reads only ref authority. Git workspace intake combines captured packs and
candidate packs under the existing byte and external-delta checks, including
checkpoint-backed views. Physical shard/xorb verification and copy-before-ref
publication remain unchanged.

`crab-auth-view` materializes a filtered view using repeated `--read-path`
and `--deny-path` rules, a scope hash, and a source repository URL. It builds
the view's Git workspace and Crab metadata and reports the resulting prefixes,
Expand Down
51 changes: 39 additions & 12 deletions crates/crab-auth-server/src/receive/capsule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crab_coordination::write_coordinator::{CommitOutcome, CoordinatedRefUpdate};
use crab_metadata::capsule_protocol::{Capsule, CapsuleRun, CapsuleSection, CapsuleSectionKind};

use super::capsule_dependencies::{
DependencyCopy, promote_dependency_copies, source_pointer_delta, verify_pointer_dependencies,
DependencyCopy, promote_dependency_copies, select_pointer_catalog, source_pointer_delta,
verify_pointer_dependencies,
};
use super::git_workspace::{materialize_capsule_source_push, verify_capsule_git_candidate};
use super::{
Expand Down Expand Up @@ -331,8 +332,10 @@ pub(super) async fn verify_capsule_candidate(
.source_root_digest
.as_deref()
.ok_or_else(|| invalid("capsule prepare record is missing its root digest"))?;
let source_view = crab_read::capsule_protocol::open_view(
let source_root = crab_metadata::capsule_protocol::load_root(ctx.router()).await?;
let source_view = crab_read::capsule_protocol::open_git_view_from_root(
ctx.router(),
source_root,
crab_read::capsule_protocol::CapsuleReadLimits {
max_capsule_bytes: MAX_PROTECTED_CAPSULE_BYTES,
max_frontier_bytes: MAX_PROTECTED_CAPSULE_BYTES,
Expand All @@ -354,16 +357,13 @@ pub(super) async fn verify_capsule_candidate(
.ok_or_else(|| invalid("protected capsule run is empty"))?;
validate_capsule_transaction(plan, candidate)?;

let candidate_delta = candidate.pointer_catalog_delta()?.unwrap_or_default();
let (changed_paths, publication, replication_objects, dependency_copies) =
match prepare.view_scope.as_ref() {
None => {
if prepared_root != plan.base_root_digest {
return Err(conflict("capsule base root differs from prepare record"));
}
let mut catalog = source_view.pointer_catalog().await?;
if let Some(delta) = candidate.pointer_catalog_delta()? {
catalog.apply(&delta)?;
}
let (changed_paths, pointers) = verify_capsule_git_candidate(
ctx.store(),
ctx.router(),
Expand All @@ -373,6 +373,15 @@ pub(super) async fn verify_capsule_candidate(
MAX_PROTECTED_CAPSULE_BYTES,
)
.await?;
let mut catalog = select_pointer_catalog(
&source_view,
ctx.router(),
&pointers,
&candidate_delta,
MAX_PROTECTED_CAPSULE_BYTES,
)
.await?;
catalog.apply(&candidate_delta)?;
let (objects, copies) =
verify_pointer_dependencies(ctx, plan, &catalog, &pointers, None).await?;
(changed_paths, candidate.clone(), objects, copies)
Expand All @@ -383,8 +392,11 @@ pub(super) async fn verify_capsule_candidate(
scope.repo_prefix.clone(),
scope.global_prefix.clone(),
);
let filtered_view = crab_read::capsule_protocol::open_view(
let filtered_root =
crab_metadata::capsule_protocol::load_root(&filtered_router).await?;
let filtered_view = crab_read::capsule_protocol::open_git_view_from_root(
&filtered_router,
filtered_root,
crab_read::capsule_protocol::CapsuleReadLimits {
max_capsule_bytes: MAX_PROTECTED_CAPSULE_BYTES,
max_frontier_bytes: MAX_PROTECTED_CAPSULE_BYTES,
Expand All @@ -395,21 +407,36 @@ pub(super) async fn verify_capsule_candidate(
return Err(conflict("filtered capsule root changed since prepare"));
}
validate_ref_heads(&prepare.view_ref_updates, filtered_view.refs())?;
let candidate_delta = candidate.pointer_catalog_delta()?.unwrap_or_default();
let mut candidate_catalog = filtered_view.pointer_catalog().await?;
candidate_catalog.apply(&candidate_delta)?;
let materialized = materialize_capsule_source_push(
ctx.router(),
&source_view,
&filtered_view,
&filtered_router,
candidate,
&plan.ref_updates,
&prepare.source_ref_updates,
&plan.transaction_id,
MAX_PROTECTED_CAPSULE_BYTES,
)
.await?;
let source_catalog = source_view.pointer_catalog().await?;
// Source and filtered captures have different authority and namespaces.
// Select each independently; source-first file policy is applied below.
let mut candidate_catalog = select_pointer_catalog(
&filtered_view,
&filtered_router,
&materialized.pointers,
&candidate_delta,
MAX_PROTECTED_CAPSULE_BYTES,
)
.await?;
candidate_catalog.apply(&candidate_delta)?;
let source_catalog = select_pointer_catalog(
&source_view,
ctx.router(),
&materialized.pointers,
&candidate_catalog,
MAX_PROTECTED_CAPSULE_BYTES,
)
.await?;
let delta = source_pointer_delta(
&source_catalog,
&candidate_catalog,
Expand Down
50 changes: 50 additions & 0 deletions crates/crab-auth-server/src/receive/capsule_dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,56 @@ pub(super) struct DependencyCopy {
digest: String,
}

pub(super) async fn select_pointer_catalog(
view: &crab_read::capsule_protocol::CapsuleRepositoryView,
layout: &crab_storage::StoreLayout<crab_storage::Store>,
pointers: &ReachablePointerScan,
candidate: &PointerCatalog,
maximum: u64,
) -> Result<PointerCatalog> {
let files = pointers
.crab_pointers
.iter()
.map(|pointer| MerkleHash::from(pointer.file_hash).hex())
.chain(candidate.files().keys().cloned())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let shards = candidate
.shards()
.keys()
.cloned()
.chain(
candidate
.files()
.values()
.map(|file| file.shard_hash().to_owned()),
)
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let xorbs = candidate
.xorbs()
.keys()
.cloned()
.chain(
candidate
.shards()
.values()
.flat_map(|shard| shard.xorb_hashes().iter().cloned()),
)
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
// Delta references may be owned by history rather than carried by the delta.
// Query every candidate identity too, so selected reads retain collision checks.
view.pointer_catalog_lookup(layout, maximum)?
.dependencies(layout.store(), &files, &shards, &xorbs)
.await
.map_err(crab_read::ReadError::from)
.map_err(Into::into)
}

pub(super) fn source_pointer_delta(
source: &PointerCatalog,
candidate: &PointerCatalog,
Expand Down
94 changes: 74 additions & 20 deletions crates/crab-auth-server/src/receive/git_workspace.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Git workspace orchestration for protected-push receive.

use std::collections::{BTreeMap, BTreeSet};
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fs;
use std::io::Write;
use std::path::Path;
Expand Down Expand Up @@ -94,17 +94,30 @@ pub(super) async fn verify_capsule_git_candidate(
let temp = tempfile::tempdir()?;
let git_dir = temp.path().join("source.git");
run_git(["init", "--bare", path_str(&git_dir)?], None)?;
crab_read::capsule_protocol::install_git_packs_with_candidates(
crab_read::capsule_protocol::install_git_packs_with_candidates_from_store(
view,
std::slice::from_ref(candidate),
router,
&git_dir,
max_input_bytes,
None,
&tokio_util::sync::CancellationToken::new(),
)
.await?;
let workspace = GitReceiveWorkspace::new(store, router, router.repo_prefix());
let paths =
workspace.compute_changed_paths_in(&git_dir, ref_updates, &BTreeMap::new(), false)?;

let refs = verify_capsule_candidate_refs(&git_dir, view, candidate).await?;
let pointers = scan_reachable_pointers_from_refs(&git_dir, &refs)?;
Ok((paths, pointers))
}

async fn verify_capsule_candidate_refs(
git_dir: &Path,
view: &crab_read::capsule_protocol::CapsuleRepositoryView,
candidate: &Capsule,
) -> Result<Vec<(String, String)>> {
let transaction = candidate.transaction()?;
let mut refs = view.refs().clone();
let mut peeled = view.peeled_refs().clone();
Expand All @@ -127,25 +140,57 @@ pub(super) async fn verify_capsule_git_candidate(
}
}
let refs = refs.into_iter().collect::<Vec<_>>();
let closures = crab_git::walk_reachable_by_ref_bounded(
&git_dir,
let reachable = crab_git::walk_reachable_by_ref_bounded(
git_dir,
&refs,
&peeled,
usize::try_from(crab_metadata::git_visibility::MAX_GIT_VISIBILITY_OBJECTS)
.map_err(|_| invalid("Git visibility object limit does not fit usize"))?,
)
.map_err(|source| AuthServerError::GitVisibilityWalk { source })?
.into_iter()
.map(|(name, reachable)| (name, reachable_object_ids(&reachable)))
.collect::<BTreeMap<_, _>>();
.map_err(|source| AuthServerError::GitVisibilityWalk { source })?;
// Pack presence is not read authority. Reintroducing an invisible object
// requires its body in the verified candidate, even if a retained base pack
// happens to contain it.
{
let visible = view.git_visibility_index().await?;
let mut allowed = visible
.objects_for_refs(view.refs().keys().map(String::as_str))
.into_iter()
.collect::<HashSet<_>>();
for pack in candidate.git_packs() {
let index = candidate.section_bytes(pack.index_section())?;
let (oids, _) = crab_git::pack_locator::sorted_object_ids_from_index_bytes(&index)
.map_err(std::io::Error::other)?;
for oid in oids {
let oid = <[u8; 20]>::try_from(oid.as_bytes()).map_err(std::io::Error::other)?;
allowed.insert(oid);
}
}
if reachable.values().any(|objects| {
objects
.commits
.iter()
.chain(&objects.trees)
.chain(&objects.blobs)
.chain(&objects.tags)
.any(|oid| !allowed.contains(oid))
}) {
return Err(invalid(
"candidate Git object is neither visible in the captured view nor supplied by its pack",
));
}
}
let closures = reachable
.into_iter()
.map(|(name, objects)| (name, reachable_object_ids(&objects)))
.collect::<BTreeMap<_, _>>();
let declared = view.candidate_git_visibility(candidate).await?;
if closures != declared {
return Err(invalid(
"candidate capsule Git visibility differs from its reachable object closure",
));
}
let pointers = scan_reachable_pointers_from_refs(&git_dir, &refs)?;
Ok((paths, pointers))
Ok(refs)
}

pub(super) struct MaterializedCapsuleSourcePush {
Expand All @@ -160,10 +205,10 @@ pub(super) async fn materialize_capsule_source_push(
source_router: &StoreLayout<Store>,
source_view: &crab_read::capsule_protocol::CapsuleRepositoryView,
filtered_view: &crab_read::capsule_protocol::CapsuleRepositoryView,
filtered_router: &StoreLayout<Store>,
candidate: &Capsule,
view_updates: &[PushRefUpdate],
source_updates: &[PushRefUpdate],
candidate_transaction_id: &str,
max_input_bytes: u64,
) -> Result<MaterializedCapsuleSourcePush> {
if view_updates.len() != source_updates.len() {
Expand All @@ -172,18 +217,14 @@ pub(super) async fn materialize_capsule_source_push(
let temp = tempfile::tempdir()?;
let git_dir = temp.path().join("source.git");
run_git(["init", "--bare", path_str(&git_dir)?], None)?;
crab_read::capsule_protocol::install_git_packs_with_candidates(
source_view,
&[],
&git_dir,
max_input_bytes,
)
.await?;
crab_read::capsule_protocol::install_git_packs_with_candidates(
crab_read::capsule_protocol::install_git_packs_with_candidates_from_store(
filtered_view,
std::slice::from_ref(candidate),
filtered_router,
&git_dir,
max_input_bytes,
None,
&tokio_util::sync::CancellationToken::new(),
)
.await?;

Expand All @@ -194,6 +235,19 @@ pub(super) async fn materialize_capsule_source_push(
);
let changed_paths =
workspace.compute_changed_paths_in(&git_dir, view_updates, filtered_view.refs(), true)?;
// Prove the candidate using only filtered and incoming objects before the
// private source ODB becomes available for source-commit synthesis.
let _ = verify_capsule_candidate_refs(&git_dir, filtered_view, candidate).await?;
crab_read::capsule_protocol::install_git_packs_with_candidates_from_store(
source_view,
&[],
source_router,
&git_dir,
max_input_bytes,
None,
&tokio_util::sync::CancellationToken::new(),
)
.await?;
let mut final_updates = Vec::with_capacity(source_updates.len());
let mut synthesized_tips = Vec::with_capacity(source_updates.len());
for (view_update, source_update) in view_updates.iter().zip(source_updates) {
Expand Down Expand Up @@ -251,7 +305,7 @@ pub(super) async fn materialize_capsule_source_push(
)?;
let transaction = CapsuleTransaction::for_protected_source(
source_view.root().digest(),
candidate_transaction_id,
candidate.transaction_id(),
final_updates
.iter()
.map(|update| {
Expand Down
15 changes: 4 additions & 11 deletions crates/crab-auth-server/src/receive/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,20 +224,13 @@ impl ReceiveContext {
},
other => other.into(),
})?;
let view = crab_read::capsule_protocol::open_view_from_root_with_control(
&self.router,
root,
crab_read::capsule_protocol::CapsuleReadLimits {
max_capsule_bytes: 2 * 1024 * 1024 * 1024,
max_frontier_bytes: 2 * 1024 * 1024 * 1024,
},
)
.await?;
let view = crab_read::capsule_protocol::open_ref_view_from_root(&self.router, root)
.await?;
build_capsule_prepare_record(
&self.repo_prefix,
&self.push_id,
view.root().root().generation(),
view.root().digest(),
view.root_snapshot().record().root().generation(),
view.root_snapshot().record().digest(),
view.refs(),
view_ref_updates,
view_scope,
Expand Down
Loading
Loading