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
3 changes: 2 additions & 1 deletion .claude/board/STATUS_BOARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Decision 1(c): observed claims, provisioning conflicts and delivery are three qu

| D-id | Deliverable | Status | Gate |
|---|---|---|---|
| **D-DSV-1** | `validate::provisioning_conflicts`: every present object's claims, separate from `address_owner` / `address_recipient` / `validate` | In PR | `tests/provisioning.rs`: delivery unchanged, conflict reported, silence half; two disable runs red |
| **D-DSV-1** | `validate::provisioning_conflicts`: every present object's claims, separate from `address_owner` / `address_recipient` / `validate` | Shipped (#1477, #1480) | `tests/provisioning.rs`: delivery unchanged, conflict reported, silence half; two disable runs red |
| **D-DSV-2** | A mail-enabled group's secondary SMTP proxies are in the Exchange address space (a group without a primary SMTP address claims them only in the provisioning plane): mail to them reaches the group (`address_owner` / `address_recipient`), and an object sharing one is an `AddressConflict` | In PR | `tests/provisioning.rs`: `mail_to_a_groups_secondary_proxy_reaches_the_group` red before; `a_groups_secondary_proxy_claims_its_address` re-pinned from owner to conflict; `a_non_mail_enabled_groups_proxy_is_only_a_provisioning_claim` red before (Codex P1) |

## D-DSP — dir-sim history that survives a restart (2026-10-11)

Expand Down
56 changes: 30 additions & 26 deletions crates/lance-graph-dir-sim/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,8 @@ pub fn address_rules(v: &View<'_>) -> Vec<Violation> {
/// The single directory object that holds `key` in the one address space
/// [`address_rules`] checks, read from the same rows: UPN, primary and
/// secondary SMTP and the routing address of address owners; the primary
/// SMTP address of every live group. `mail` holds nothing.
/// and secondary SMTP addresses of every mail-enabled group (one with a
/// primary SMTP address). `mail` holds nothing.
///
/// - `Ok(None)`: no live directory object holds the key.
/// - `Ok(Some(owner))`: exactly one does, under one or more roles (a user
Expand Down Expand Up @@ -541,17 +542,24 @@ pub fn address_recipient_in(
fn address_rows(v: &View<'_>, out: &mut Vec<Violation>) -> Vec<(u32, Guid128, AddressRole)> {
// The UPN is held by an enabled account or a live recipient; the mail
// addresses are provisioned by the recipient type alone.
rows_over(v, &v.owner_users(), &v.mail_owner_users(), out)
rows_over(v, &v.owner_users(), &v.mail_owner_users(), false, out)
}

/// The `(key, holder, role)` rows of the users in `owners` (their UPN)
/// and `mail_owners` (their primary and secondary SMTP and routing
/// address), and of every live group (its primary SMTP), sorted and
/// deduplicated. Routing violations of `mail_owners` go to `out`.
/// address), and of every live group (its primary and secondary SMTP),
/// sorted and deduplicated. Routing violations of `mail_owners` go to `out`.
///
/// `every_group_proxy` decides which groups' secondary proxies count: in
/// Exchange (`false`) only a mail-enabled group's, the one that has a
/// primary SMTP address, since a group that is not mail-enabled holds and
/// receives at nothing; in the provisioning plane (`true`) every present
/// group's, since Entra keeps proxy addresses unique whatever the object.
fn rows_over(
v: &View<'_>,
owners: &[u64],
mail_owners: &[u64],
every_group_proxy: bool,
out: &mut Vec<Violation>,
) -> Vec<(u32, Guid128, AddressRole)> {
let p = &v.snap.users;
Expand Down Expand Up @@ -615,19 +623,30 @@ fn rows_over(
out.push(Violation::RoutingMismatch { node: g });
}
}
// A mail-enabled group's primary SMTP address is in the same space: a
// group is a recipient too (a distribution list), and Exchange refuses
// an address another object holds, whatever kind that object is.
// A mail-enabled group's SMTP addresses are in the same space: a group
// is a recipient too (a distribution list), Exchange delivers mail sent
// to any of its SMTP proxies, and it refuses an address another object
// holds, whatever kind that object is. A group created in a version
// carries no secondary proxies (a `Change` sets none), so only observed
// groups have proxy rows.
let gp = &v.snap.group_proxies;
for i in 0..v.groups_len() {
let Some(g) = v.guid_in(NodeKind::Group, i) else {
continue;
};
if let Some((_, k)) = v
let primary = v
.attr_ids((NodeKind::Group, i), Attribute::PrimarySmtp)
.filter(|&(_, k)| k != NONE)
{
.filter(|&(_, k)| k != NONE);
if let Some((_, k)) = primary {
rows.push((k, g, AddressRole::PrimarySmtp));
}
if (every_group_proxy || primary.is_some()) && i < v.snap.groups.len() {
for r in gp.owner_rows(i as u32) {
if gp.meta[r] == meta::SMTP_SECONDARY {
rows.push((gp.key[r], g, AddressRole::SecondarySmtp));
}
}
}
}
rows.sort_unstable();
rows.dedup();
Expand Down Expand Up @@ -679,22 +698,7 @@ pub struct ProvisioningConflict {
/// real tenant and are not in this plane.
pub fn provisioning_conflicts(v: &View<'_>) -> Vec<ProvisioningConflict> {
let present = v.existing(NodeKind::User);
let mut rows = rows_over(v, &present, &present, &mut Vec::new());
// A group's secondary SMTP proxies claim too. A group created in a
// version carries none (a `Change` sets no proxy).
let gp = &v.snap.group_proxies;
for i in 0..v.snap.groups.len() {
let Some(g) = v.guid_in(NodeKind::Group, i) else {
continue;
};
for r in gp.owner_rows(i as u32) {
if gp.meta[r] == meta::SMTP_SECONDARY {
rows.push((gp.key[r], g, AddressRole::SecondarySmtp));
}
}
}
rows.sort_unstable();
rows.dedup();
let rows = rows_over(v, &present, &present, true, &mut Vec::new());
rows.chunk_by(|a, b| a.0 == b.0)
.filter(|run| run.iter().any(|r| r.1 != run[0].1))
.map(|run| ProvisioningConflict {
Expand Down
80 changes: 78 additions & 2 deletions crates/lance-graph-dir-sim/tests/provisioning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,11 @@ fn distinct_claims_and_one_object_under_two_roles_are_not_conflicts() {
}

// A group's secondary SMTP proxy claims like any other address: another
// object carrying it is a provisioning conflict. Delivery still reads a
// group by its primary address only, so the user keeps the address there.
// object carrying it is a provisioning conflict. It is also held in
// Exchange (mail to it reaches the group), so a user carrying the same
// address shares the key and the address names no single recipient.
// (Before #45 delivery read a group by its primary address only, and this
// test pinned the user as the owner.)
#[test]
fn a_groups_secondary_proxy_claims_its_address() {
let mut grp = group("team@x.test");
Expand All @@ -148,7 +151,80 @@ fn a_groups_secondary_proxy_claims_its_address() {
st.key_of(id).unwrap()
};
let view = st.view(v).unwrap();
let holders = vec![
(g(0xA1), AddressRole::PrimarySmtp),
(g(0xB0), AddressRole::SecondarySmtp),
];
assert_eq!(
address_owner(&view, key),
Err(Violation::AddressConflict {
key,
holders: holders.clone()
})
);
assert_eq!(
address_recipient(&view, key),
Err(Violation::AddressConflict {
key,
holders: holders.clone()
})
);
assert_eq!(
provisioning_conflicts(&view),
vec![ProvisioningConflict {
key,
claims: vec![
(g(0xA1), AddressRole::PrimarySmtp),
(g(0xB0), AddressRole::SecondarySmtp),
],
}]
);
}

// Exchange delivers mail sent to any of a distribution group's SMTP
// proxies, not only its primary address: the secondary proxy is held by
// the group and delivers to it.
#[test]
fn mail_to_a_groups_secondary_proxy_reaches_the_group() {
let mut grp = group("team@x.test");
grp.proxies = vec!["smtp:sales@x.test".into()];
let (mut st, v) = store(vec![(g(0xB0), grp)]);
let key = {
let id = st.intern("sales@x.test");
st.key_of(id).unwrap()
};
let view = st.view(v).unwrap();
assert_eq!(address_owner(&view, key), Ok(Some(g(0xB0))));
assert_eq!(address_recipient(&view, key), Ok(Some(g(0xB0))));
assert_eq!(st.validate(v).unwrap(), vec![]);
}

// A group with no primary SMTP address is not mail-enabled: its leftover
// secondary proxy is not held in Exchange and delivers nowhere, so a user
// carrying the same address keeps it. The leftover still claims the key in
// the provisioning plane.
#[test]
fn a_non_mail_enabled_groups_proxy_is_only_a_provisioning_claim() {
let mut grp = ObservedNode::group();
grp.proxies = vec!["smtp:sales@x.test".into()];
let (mut st, v) = store(vec![
(g(0xB0), grp),
(
g(0xA1),
ObservedNode::user("alice.upn@x.test", "sales@x.test"),
),
]);
let key = {
let id = st.intern("sales@x.test");
st.key_of(id).unwrap()
};
let view = st.view(v).unwrap();
assert!(
!view.is_mail_recipient(&g(0xB0)),
"fixture: not mail-enabled"
);
assert_eq!(address_owner(&view, key), Ok(Some(g(0xA1))));
assert_eq!(address_recipient(&view, key), Ok(Some(g(0xA1))));
assert_eq!(
provisioning_conflicts(&view),
vec![ProvisioningConflict {
Expand Down
Loading