Skip to content
Merged
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
37 changes: 23 additions & 14 deletions .github/workflows/aien-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,23 +82,28 @@ jobs:
ref: ${{ env.AIEN_TEST_PIN }}
path: .aien-test-src

- name: Cache the pinned aien-test build
- name: Move the pinned source outside the candidate Cargo configuration
if: steps.anchors.outputs.enrolled == 'true'
run: mv .aien-test-src "$RUNNER_TEMP/aien-test-src"

# Cache dependencies only, never verifier executables or candidate-controlled target output.
- name: Cache the pinned aien-test dependencies
uses: actions/cache@v4
if: steps.anchors.outputs.enrolled == 'true'
with:
path: |
~/.cargo/registry
~/.cargo/git
.aien-test-src/target
key: aien-test-${{ runner.os }}-${{ env.AIEN_TEST_PIN }}
key: aien-test-isolated-v1-${{ runner.os }}-${{ env.AIEN_TEST_PIN }}

- name: Build aien-test from the pin (never from the tree under test), then remove the source
- name: Build aien-test outside the tree under test
if: steps.anchors.outputs.enrolled == 'true'
run: |
cargo build --release -p aien-test --manifest-path .aien-test-src/Cargo.toml
mkdir -p "$RUNNER_TEMP/bin" && cp .aien-test-src/target/release/aien-test "$RUNNER_TEMP/bin/aien-test"
cd "$RUNNER_TEMP/aien-test-src"
cargo build --release -p aien-test --target-dir "$RUNNER_TEMP/aien-test-target"
mkdir -p "$RUNNER_TEMP/bin" && cp "$RUNNER_TEMP/aien-test-target/release/aien-test" "$RUNNER_TEMP/bin/aien-test"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
rm -rf .aien-test-src
cd "$GITHUB_WORKSPACE"
git status --porcelain --untracked-files=normal

- name: Portable smoke set (gates whose requires is only portable, named at the base ref)
Expand Down Expand Up @@ -172,21 +177,25 @@ jobs:
ref: ${{ env.AIEN_TEST_PIN }}
path: .aien-test-src

- name: Cache the pinned aien-test build
- name: Move the pinned source outside the candidate Cargo configuration
run: mv .aien-test-src "$RUNNER_TEMP/aien-test-src"

# Cache dependencies only, never verifier executables or candidate-controlled target output.
- name: Cache the pinned aien-test dependencies
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
.aien-test-src/target
key: aien-test-${{ runner.os }}-${{ env.AIEN_TEST_PIN }}
key: aien-test-isolated-v1-${{ runner.os }}-${{ env.AIEN_TEST_PIN }}

- name: Build aien-test from the pin (never from the tree under test), then remove the source
- name: Build aien-test outside the tree under test
run: |
cargo build --release -p aien-test --manifest-path .aien-test-src/Cargo.toml
mkdir -p "$RUNNER_TEMP/bin" && cp .aien-test-src/target/release/aien-test "$RUNNER_TEMP/bin/aien-test"
cd "$RUNNER_TEMP/aien-test-src"
cargo build --release -p aien-test --target-dir "$RUNNER_TEMP/aien-test-target"
mkdir -p "$RUNNER_TEMP/bin" && cp "$RUNNER_TEMP/aien-test-target/release/aien-test" "$RUNNER_TEMP/bin/aien-test"
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
rm -rf .aien-test-src
cd "$GITHUB_WORKSPACE"
git status --porcelain --untracked-files=normal

- name: Fetch the signed Spark bundle for the tagged commit
Expand Down
4 changes: 3 additions & 1 deletion crates/synapse-api/src/advisory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,9 @@ impl SynapseService {
let mut stack = vec![start];
while let Some(id) = stack.pop() {
if out.insert(id) {
let cs = self.store.get_changeset(id, None, snap)?;
let cs = self
.store
.get_changeset_in_repo(&self.repo_id, id, None, snap)?;
stack.extend(cs.envelope.parent_changesets.iter().copied());
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/synapse-api/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ impl SynapseService {
) -> SynapseResult<EvidenceBase> {
let envelope = self
.store
.get_changeset(changeset_id, None, snapshot_seq)?
.get_changeset_in_repo(&self.repo_id, changeset_id, None, snapshot_seq)?
.envelope;
let base_tree = self.base_tree(&envelope, snapshot_seq)?;
// The changeset's own tree is the v1 input root: the tree the runner tested.
Expand Down
252 changes: 244 additions & 8 deletions crates/synapse-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,8 @@ impl SynapseService {
now_ms,
)?;
let (cs_id, min_seq, snap_seq) = req.payload;
self.store.get_changeset(cs_id, min_seq, snap_seq)
self.store
.get_changeset_in_repo(&self.repo_id, cs_id, min_seq, snap_seq)
}

/// Read a file's raw bytes at `TreeTarget` without cloning or checking out a working tree (`Capability::Read`).
Expand Down Expand Up @@ -970,7 +971,12 @@ impl SynapseService {
let mut stack = vec![head_id];
while let Some(curr) = stack.pop() {
if set.insert(curr) {
let stored = self.store.get_changeset(curr, None, Some(snap_seq))?;
let stored = self.store.get_changeset_in_repo(
&self.repo_id,
curr,
None,
Some(snap_seq),
)?;
for p in &stored.envelope.parent_changesets {
stack.push(*p);
}
Expand Down Expand Up @@ -1037,7 +1043,9 @@ impl SynapseService {
now_ms,
)?;
let (cs_id, snap_seq) = req.payload;
let stored = self.store.get_changeset(cs_id, None, snap_seq)?;
let stored = self
.store
.get_changeset_in_repo(&self.repo_id, cs_id, None, snap_seq)?;
let github_projection = self
.store
.github_projections()
Expand Down Expand Up @@ -1089,15 +1097,23 @@ impl SynapseService {
now_ms,
)?;
let snap_seq = self.validate_page_request(&req.payload.page)?;
let cand_stored = self
.store
.get_changeset(req.payload.candidate, None, Some(snap_seq))?;
let cand_stored = self.store.get_changeset_in_repo(
&self.repo_id,
req.payload.candidate,
None,
Some(snap_seq),
)?;

let peers: Vec<StoredChangeset> = if let Some(ref explicit) = req.payload.against {
let mut list = Vec::with_capacity(explicit.len());
for &id in explicit {
if id != req.payload.candidate {
list.push(self.store.get_changeset(id, None, Some(snap_seq))?);
list.push(self.store.get_changeset_in_repo(
&self.repo_id,
id,
None,
Some(snap_seq),
)?);
}
}
list
Expand Down Expand Up @@ -1472,7 +1488,9 @@ impl SynapseService {
TreeTarget::Changeset(cs_id) => cs_id,
};
// Visibility at `snapshot_seq` is checked on every lookup, memoized or not.
let stored = self.store.get_changeset(cs_id, None, snapshot_seq)?;
let stored = self
.store
.get_changeset_in_repo(&self.repo_id, cs_id, None, snapshot_seq)?;
if !force {
if let Some(root) = self.trees.memo_root(cs_id) {
return Ok(root);
Expand Down Expand Up @@ -1820,6 +1838,224 @@ mod tests {
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn repository_grant_cannot_read_foreign_changesets_in_mixed_store() {
let dir = temp_dir("mixed-repositories");
let a = RepoId::new("example/repo-a").unwrap();
let b = RepoId::new("example/repo-b").unwrap();
let mut svc = SynapseService::open(
&dir,
a.clone(),
FsyncPolicy::EveryCommit,
RecoveryPolicy::StrictFailClosed,
StoreLimits::default(),
)
.unwrap();
let authority = SigningKey::from_bytes(&[11; 32]);
let agent = SigningKey::from_bytes(&[12; 32]);
svc.register_authority(encode_did_key_ed25519(
&authority.verifying_key().to_bytes(),
));
let grant = AgentCapabilityGrant::issue(
&authority,
[1; 32],
a.clone(),
encode_did_key_ed25519(&agent.verifying_key().to_bytes()),
vec![Capability::Read],
1_000_000,
100,
60_000,
0,
100_000,
);
let base = GitOid([7; 20]);
svc.register_base_tree(base, vec![]).unwrap();
let make = |repo_id: RepoId, tag: u8, parents: Vec<ChangesetId>| {
let mut env = ChangesetEnvelope {
version: SYNAPSE_ENVELOPE_VERSION,
repo_id,
base_git_oid: base,
parent_changesets: parents,
author_did: String::new(),
idempotency_key: IdempotencyKey([tag; 32]),
created_at_ms: 1_000,
intent_summary: "security test".into(),
mutations: vec![PathMutation {
path: "private.txt".into(),
op: MutationOp::Delete,
}],
evidence_receipts: vec![],
signature: vec![],
};
env.sign(&agent);
env
};
let own = match svc
.store_mut()
.ingest_changeset(make(a.clone(), 1, vec![]), &[])
.unwrap()
{
IngestOutcome::Committed { changeset_id, .. } => changeset_id,
_ => unreachable!(),
};
let foreign = match svc
.store_mut()
.ingest_changeset(make(b, 2, vec![]), &[])
.unwrap()
{
IngestOutcome::Committed { changeset_id, .. } => changeset_id,
_ => unreachable!(),
};
let foreign_child = match svc
.store_mut()
.ingest_changeset(
make(RepoId::new("example/repo-b").unwrap(), 3, vec![foreign]),
&[],
)
.unwrap()
{
IngestOutcome::Committed { changeset_id, .. } => changeset_id,
_ => unreachable!(),
};
let mut nonce = 0u8;
macro_rules! request {
($payload:expr) => {{
nonce += 1;
AuthenticatedRequest::sign_new(
&agent,
grant.clone(),
a.clone(),
Capability::Read,
[nonce; 32],
2_000,
0,
$payload,
)
.unwrap()
}};
}
assert_eq!(
svc.get(&request!((own, None, None)), 2_000)
.unwrap()
.changeset_id,
own
);
assert_eq!(
svc.get(&request!((foreign, None, None)), 2_000)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.get(&request!((ChangesetId([0xFF; 32]), None, None)), 2_000)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.provenance(&request!((foreign, None)), 2_000)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.tree(
&request!(TreeQueryPayload {
target: TreeTarget::Changeset(foreign),
path_prefix: None,
page: PageRequest::default(),
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.tree(
&request!(TreeQueryPayload {
target: TreeTarget::Changeset(foreign_child),
path_prefix: None,
page: PageRequest::default(),
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.evidence_base(
&request!(EvidenceBaseQueryPayload {
changeset_id: foreign,
snapshot_seq: None,
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.changeset_advisory(
&request!(ChangesetAdvisoryPayload {
base: TreeTarget::BaseGitCommit(base),
latest: Some(TreeTarget::Changeset(foreign)),
paths: vec!["private.txt".into()],
agent: "reader".into(),
snapshot_seq: None,
}),
None,
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.history(
&request!(HistoryQueryPayload {
ancestor_of: Some(foreign),
path_filter: None,
page: PageRequest::default(),
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
assert_eq!(
svc.history(
&request!(HistoryQueryPayload {
ancestor_of: Some(foreign_child),
path_filter: None,
page: PageRequest::default(),
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
for (candidate, against) in [(foreign, vec![own]), (own, vec![foreign])] {
assert_eq!(
svc.conflicts(
&request!(ConflictQueryPayload {
candidate,
against: Some(against),
page: PageRequest::default(),
}),
2_000
)
.unwrap_err()
.code,
RefusalCode::ChangesetNotFound
);
}
assert!(svc.provenance(&request!((own, None)), 2_000).is_ok());
let _ = fs::remove_dir_all(dir);
}

#[test]
fn test_virtual_tree_and_diff_parity_against_git_cli() {
Expand Down
Loading
Loading