Skip to main content

kcode_k1_access/
lib.rs

1use std::{path::Path, sync::Arc};
2
3use kcode_k1_access_driver::K1AccessDriver;
4use kcode_k1_groups::K1Groups;
5use kcode_k1_peering::K1Peering;
6use kcode_k1_txn_ordering::K1TxnOrdering;
7
8pub use kcode_k1_access_types::{
9    AccessCheck, AccessContext, AccessId, AccessPolicy, AccessRevision, Authority,
10    FilteredAuthorities, GroupId, ModelId, ProfileId, SubsystemId, Target, TxId, UserId,
11    ViewerSubject,
12};
13pub use kcode_k1_groups::{ALL_MODELS, ALL_MODELS_MEMBER, ALL_USERS, LOCAL_MODELS, SentinelGroup};
14
15const ACCESS_UNAVAILABLE: &str = "access is unavailable";
16const ACCESS_GROUP_UNAVAILABLE: &str = "access group is unavailable";
17const FILTERED_AUTHORITY: &str = "access authority is filtered";
18
19pub struct K1Access {
20    driver: K1AccessDriver,
21    groups: Arc<K1Groups>,
22}
23
24impl K1Access {
25    pub fn open(
26        root: &Path,
27        ordering: Arc<K1TxnOrdering>,
28        peering: Arc<K1Peering>,
29        groups: Arc<K1Groups>,
30    ) -> Result<Self, String> {
31        Ok(Self {
32            driver: K1AccessDriver::open(root, ordering, peering)?,
33            groups,
34        })
35    }
36
37    pub fn create(
38        &self,
39        context: &AccessContext,
40        target: Target,
41        profile_id: ProfileId,
42        policy: AccessPolicy,
43    ) -> Result<AccessRevision, String> {
44        require_visible_authority(context, &policy)?;
45        self.driver.create(target, profile_id, policy)
46    }
47
48    /// Performs a raw trusted lookup without authorization, filtering, or concealment.
49    ///
50    /// This internal/public-library primitive must not be exposed directly through an
51    /// untrusted or user-facing interface.
52    pub fn profile_id(&self, access_id: AccessId) -> Result<Option<ProfileId>, String> {
53        self.driver.profile_id(access_id)
54    }
55
56    pub fn replace_policy(
57        &self,
58        context: &AccessContext,
59        access_id: AccessId,
60        editors: Vec<Authority>,
61        viewers: Vec<ViewerSubject>,
62    ) -> Result<AccessRevision, String> {
63        let memberships = self.groups.memberships(context.user(), context.model())?;
64        let witness = self
65            .driver
66            .edit_witness(context, access_id, memberships.user_groups())?;
67        let witness = witness.ok_or_else(|| ACCESS_UNAVAILABLE.to_owned())?;
68        let (editors, viewers) = normalize_replacement(context.user(), editors, viewers)?;
69        self.driver.replace(
70            access_id,
71            context.user(),
72            memberships.revision(),
73            witness,
74            editors,
75            viewers,
76        )
77    }
78
79    pub fn check(
80        &self,
81        context: &AccessContext,
82        access_id: AccessId,
83        expected_subsystem: SubsystemId,
84    ) -> Result<AccessCheck, String> {
85        if self
86            .driver
87            .discovery_missing(context, access_id, expected_subsystem)?
88        {
89            self.driver.ensure_discovery(access_id)?;
90        }
91        let memberships = self.groups.memberships(context.user(), context.model())?;
92        self.driver.check(
93            context,
94            access_id,
95            expected_subsystem,
96            memberships.user_groups(),
97            memberships.model_groups(),
98            memberships.revision(),
99        )
100    }
101
102    pub fn check_many(
103        &self,
104        context: &AccessContext,
105        access_ids: &[AccessId],
106        expected_subsystem: SubsystemId,
107    ) -> Result<Vec<AccessCheck>, String> {
108        if access_ids.is_empty() {
109            return Ok(Vec::new());
110        }
111        let memberships = self.groups.memberships(context.user(), context.model())?;
112        self.driver.check_many(
113            context,
114            access_ids,
115            expected_subsystem,
116            memberships.user_groups(),
117            memberships.model_groups(),
118            memberships.revision(),
119        )
120    }
121
122    pub fn resolve_visible_targets(
123        &self,
124        context: &AccessContext,
125        targets: &[Target],
126        expected_subsystem: SubsystemId,
127    ) -> Result<Vec<Option<AccessId>>, String> {
128        if targets.is_empty() {
129            return Ok(Vec::new());
130        }
131        let memberships = self.groups.memberships(context.user(), context.model())?;
132        self.driver.resolve_visible_targets(
133            context,
134            targets,
135            expected_subsystem,
136            memberships.user_groups(),
137            memberships.model_groups(),
138        )
139    }
140
141    pub fn list_user(
142        &self,
143        context: &AccessContext,
144        expected_subsystem: SubsystemId,
145    ) -> Result<Vec<AccessId>, String> {
146        self.driver.discovered_for_user(context, expected_subsystem)
147    }
148
149    pub fn list_group(
150        &self,
151        context: &AccessContext,
152        group: GroupId,
153        expected_subsystem: SubsystemId,
154    ) -> Result<Vec<AccessId>, String> {
155        let groups = self.groups.groups_for_user(context.user())?;
156        require_group(&groups, group)?;
157        self.driver
158            .discovered_for_group(context, group, expected_subsystem)
159    }
160
161    pub fn list_user_group_targets(
162        &self,
163        context: &AccessContext,
164        expected_subsystem: SubsystemId,
165    ) -> Result<Vec<(AccessId, Target)>, String> {
166        let memberships = self.groups.memberships(context.user(), context.model())?;
167        self.driver.discovered_targets_for_user_and_groups(
168            context,
169            memberships.user_groups(),
170            expected_subsystem,
171        )
172    }
173}
174
175fn require_visible_authority(context: &AccessContext, policy: &AccessPolicy) -> Result<(), String> {
176    if context.filter().contains(policy.authority()) {
177        Err(FILTERED_AUTHORITY.to_owned())
178    } else {
179        Ok(())
180    }
181}
182
183fn normalize_replacement(
184    actor: UserId,
185    mut editors: Vec<Authority>,
186    mut viewers: Vec<ViewerSubject>,
187) -> Result<(Vec<Authority>, Vec<ViewerSubject>), String> {
188    AccessPolicy::new(Authority::User(actor), editors.clone(), viewers.clone())?;
189    editors.sort_unstable();
190    editors.dedup();
191    viewers.sort_unstable();
192    viewers.dedup();
193    viewers.retain(|viewer| match viewer {
194        ViewerSubject::User(user) => editors.binary_search(&Authority::User(*user)).is_err(),
195        ViewerSubject::Group(group) => editors.binary_search(&Authority::Group(*group)).is_err(),
196        ViewerSubject::Model(_) => true,
197    });
198    Ok((editors, viewers))
199}
200
201fn require_group(groups: &[GroupId], group: GroupId) -> Result<(), String> {
202    if groups.contains(&group) {
203        Ok(())
204    } else {
205        Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn txid(byte: u8) -> TxId {
214        TxId::from_bytes([byte; 12])
215    }
216
217    fn user(byte: u8) -> UserId {
218        UserId::from_tx_id(txid(byte))
219    }
220
221    fn group(byte: u8) -> GroupId {
222        GroupId::new(txid(byte))
223    }
224
225    fn model(byte: u8) -> ModelId {
226        ModelId::from_bytes([byte; 32])
227    }
228
229    #[test]
230    fn filtered_create_authority_is_rejected() {
231        let authority = Authority::Group(group(7));
232        let context = AccessContext::new(
233            user(1),
234            model(2),
235            FilteredAuthorities::new(vec![authority]).unwrap(),
236        )
237        .unwrap();
238        let policy = AccessPolicy::new(authority, Vec::new(), Vec::new()).unwrap();
239        assert_eq!(
240            require_visible_authority(&context, &policy),
241            Err(FILTERED_AUTHORITY.to_owned())
242        );
243    }
244
245    #[test]
246    fn replacement_lists_are_canonical_and_editor_viewers_are_removed() {
247        let editor_user = user(3);
248        let editor_group = group(4);
249        let visible_model = model(5);
250        let (editors, viewers) = normalize_replacement(
251            user(1),
252            vec![
253                Authority::Group(editor_group),
254                Authority::User(editor_user),
255                Authority::Group(editor_group),
256            ],
257            vec![
258                ViewerSubject::Model(visible_model),
259                ViewerSubject::User(editor_user),
260                ViewerSubject::Group(editor_group),
261                ViewerSubject::Model(visible_model),
262            ],
263        )
264        .unwrap();
265        assert_eq!(
266            editors,
267            vec![Authority::User(editor_user), Authority::Group(editor_group)]
268        );
269        assert_eq!(viewers, vec![ViewerSubject::Model(visible_model)]);
270    }
271
272    #[test]
273    fn replacement_rejects_sentinel_editors_and_all_models_member() {
274        assert!(
275            normalize_replacement(user(1), vec![Authority::Group(ALL_USERS)], Vec::new()).is_err()
276        );
277        assert!(
278            normalize_replacement(
279                user(1),
280                Vec::new(),
281                vec![ViewerSubject::Model(ALL_MODELS_MEMBER)]
282            )
283            .is_err()
284        );
285    }
286
287    #[test]
288    fn requested_group_requires_current_membership() {
289        let requested = group(1);
290        assert_eq!(require_group(&[requested], requested), Ok(()));
291        assert_eq!(
292            require_group(&[group(2), group(3)], requested),
293            Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
294        );
295    }
296}