diff --git a/crates/lance-graph-dir-sim/src/view.rs b/crates/lance-graph-dir-sim/src/view.rs index a990ace6f..ef041a64e 100644 --- a/crates/lance-graph-dir-sim/src/view.rs +++ b/crates/lance-graph-dir-sim/src/view.rs @@ -24,12 +24,12 @@ //! every live plane. Ordinals are valid only inside one view; the overlay //! keeps identities wherever an ordinal could shift. +use crate::GroupWhere; use crate::snapshot::{ - bit, clear_bit, is_mail_recipient, is_owner, set_bit, Dicts, GroupOrdinal, Population, - Snapshot, UserOrdinal, MAX_GROUPS, MAX_USERS, NONE, + Dicts, GroupOrdinal, MAX_GROUPS, MAX_USERS, NONE, Population, Snapshot, UserOrdinal, bit, + clear_bit, is_mail_recipient, is_owner, set_bit, }; -use crate::GroupWhere; -use lance_graph_mask_risc::{words_for, Foreign, LaneRef, Planes}; +use lance_graph_mask_risc::{Foreign, LaneRef, Planes, words_for}; use lance_graph_quack::{Cmp, Col, Filter, Mask}; use ogar_dir_core::{Dn128, Guid128}; use ogar_dir_sim::{ @@ -548,6 +548,22 @@ impl<'s> View<'s> { self.members_walk(group, Some(&allowed)) } + /// [`Self::members_transitive`] through the groups in `within` only: + /// `group` itself and every nested group on the way must be in the set. + /// For a consumer whose own notion of a group is an explicit list (a CRM + /// team hierarchy, where a team's sub-teams are nested groups but an + /// unrelated security group nested in a team is not a sub-team). A + /// GUID in `within` that is not an existing group is ignored. + pub fn members_transitive_within(&self, group: &Guid128, within: &[Guid128]) -> Vec { + let mut allowed = vec![0u64; words_for(self.groups_len())]; + for g in within { + if let Some(o) = self.group_ordinal(g) { + set_bit(&mut allowed, usize::from(o.0)); + } + } + self.members_walk(group, Some(&allowed)) + } + /// The groups that satisfy `w`, as a bitmap over group ordinals. fn groups_bitmap(&self, w: &GroupWhere) -> Vec { let mut bits = vec![0u64; words_for(self.groups_len())]; diff --git a/crates/lance-graph-dir-sim/tests/nested_groups.rs b/crates/lance-graph-dir-sim/tests/nested_groups.rs index 7a9f4da5f..3ab70ca24 100644 --- a/crates/lance-graph-dir-sim/tests/nested_groups.rs +++ b/crates/lance-graph-dir-sim/tests/nested_groups.rs @@ -255,11 +255,13 @@ fn empty_property_junctions_are_identities() { assert_eq!(selected(&view, &GroupWhere::And(vec![])), all); assert!(selected(&view, &GroupWhere::Or(vec![])).is_empty()); assert_eq!(selected(&view, ¬(GroupWhere::Or(vec![]))), all); - assert!(selected( - &view, - &GroupWhere::And(vec![GroupWhere::Or(vec![]), is(GroupProperty::MailEnabled)]) - ) - .is_empty()); + assert!( + selected( + &view, + &GroupWhere::And(vec![GroupWhere::Or(vec![]), is(GroupProperty::MailEnabled)]) + ) + .is_empty() + ); } // Mail is chained addressing, never inherited: mail to S1 reaches its @@ -387,3 +389,32 @@ fn ad_group_type_sets_the_flag() { .collect(); assert_eq!(flags, vec![Some(true), Some(false), None]); } + +#[test] +fn members_transitive_within_walks_only_the_listed_groups() { + let mut st = VersionStore::new(); + let v = st.observe("lab", 1, observed()).unwrap(); + let view = st.view(v).unwrap(); + // S1 alone: its direct user u1; S2, D1 and UNREAD are not walked. + assert_eq!( + view.members_transitive_within(&g(S1), &[g(S1)]), + vec![g(U1)] + ); + // S1 through D1: u3, and S3 (not listed) is not walked, so u4 is not. + assert_eq!( + view.members_transitive_within(&g(S1), &[g(S1), g(D1)]), + vec![g(U1), g(U3)] + ); + // Every group listed: the same as the unrestricted walk. + let all = [g(S1), g(S2), g(S3), g(D1), g(UNREAD)]; + assert_eq!( + view.members_transitive_within(&g(S1), &all), + view.members_transitive(&g(S1)) + ); + // The start group not listed: nothing. A non-group GUID is ignored. + assert!(view.members_transitive_within(&g(S1), &[g(S2)]).is_empty()); + assert_eq!( + view.members_transitive_within(&g(S1), &[g(S1), g(U2)]), + vec![g(U1)] + ); +}