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    /// Performs a raw trusted metadata lookup without authorization, filtering, or concealment.
57    ///
58    /// This internal/public-library primitive must not be exposed directly through an
59    /// untrusted or user-facing interface.
60    pub fn authority(&self, access_id: AccessId) -> Result<Option<Authority>, String> {
61        self.driver.authority(access_id)
62    }
63
64    pub fn controller_profile_ids(
65        &self,
66        context: &AccessContext,
67        access_ids: &[AccessId],
68    ) -> Result<Vec<Option<ProfileId>>, String> {
69        if access_ids.is_empty() {
70            return Ok(Vec::new());
71        }
72        let memberships = self.groups.memberships(context.user(), context.model())?;
73        self.driver
74            .controller_profile_ids(context, access_ids, memberships.user_groups())
75    }
76
77    pub fn replace_policy(
78        &self,
79        context: &AccessContext,
80        access_id: AccessId,
81        editors: Vec<Authority>,
82        viewers: Vec<ViewerSubject>,
83    ) -> Result<AccessRevision, String> {
84        let memberships = self.groups.memberships(context.user(), context.model())?;
85        let witness = self
86            .driver
87            .edit_witness(context, access_id, memberships.user_groups())?;
88        let witness = witness.ok_or_else(|| ACCESS_UNAVAILABLE.to_owned())?;
89        let (editors, viewers) = normalize_replacement(context.user(), editors, viewers)?;
90        self.driver.replace(
91            access_id,
92            context.user(),
93            memberships.revision(),
94            witness,
95            editors,
96            viewers,
97        )
98    }
99
100    pub fn check(
101        &self,
102        context: &AccessContext,
103        access_id: AccessId,
104        expected_subsystem: SubsystemId,
105    ) -> Result<AccessCheck, String> {
106        if self
107            .driver
108            .discovery_missing(context, access_id, expected_subsystem)?
109        {
110            self.driver.ensure_discovery(access_id)?;
111        }
112        let memberships = self.groups.memberships(context.user(), context.model())?;
113        self.driver.check(
114            context,
115            access_id,
116            expected_subsystem,
117            memberships.user_groups(),
118            memberships.model_groups(),
119            memberships.revision(),
120        )
121    }
122
123    pub fn check_many(
124        &self,
125        context: &AccessContext,
126        access_ids: &[AccessId],
127        expected_subsystem: SubsystemId,
128    ) -> Result<Vec<AccessCheck>, String> {
129        if access_ids.is_empty() {
130            return Ok(Vec::new());
131        }
132        let memberships = self.groups.memberships(context.user(), context.model())?;
133        self.driver.check_many(
134            context,
135            access_ids,
136            expected_subsystem,
137            memberships.user_groups(),
138            memberships.model_groups(),
139            memberships.revision(),
140        )
141    }
142
143    pub fn resolve_visible_targets(
144        &self,
145        context: &AccessContext,
146        targets: &[Target],
147        expected_subsystem: SubsystemId,
148    ) -> Result<Vec<Option<AccessId>>, String> {
149        if targets.is_empty() {
150            return Ok(Vec::new());
151        }
152        let memberships = self.groups.memberships(context.user(), context.model())?;
153        self.driver.resolve_visible_targets(
154            context,
155            targets,
156            expected_subsystem,
157            memberships.user_groups(),
158            memberships.model_groups(),
159        )
160    }
161
162    pub fn list_user(
163        &self,
164        context: &AccessContext,
165        expected_subsystem: SubsystemId,
166    ) -> Result<Vec<AccessId>, String> {
167        self.driver.discovered_for_user(context, expected_subsystem)
168    }
169
170    pub fn list_group(
171        &self,
172        context: &AccessContext,
173        group: GroupId,
174        expected_subsystem: SubsystemId,
175    ) -> Result<Vec<AccessId>, String> {
176        let groups = self.groups.groups_for_user(context.user())?;
177        require_group(&groups, group)?;
178        self.driver
179            .discovered_for_group(context, group, expected_subsystem)
180    }
181
182    pub fn list_user_group_targets(
183        &self,
184        context: &AccessContext,
185        expected_subsystem: SubsystemId,
186    ) -> Result<Vec<(AccessId, Target)>, String> {
187        let memberships = self.groups.memberships(context.user(), context.model())?;
188        self.driver.discovered_targets_for_user_and_groups(
189            context,
190            memberships.user_groups(),
191            expected_subsystem,
192        )
193    }
194}
195
196fn require_visible_authority(context: &AccessContext, policy: &AccessPolicy) -> Result<(), String> {
197    if context.filter().contains(policy.authority()) {
198        Err(FILTERED_AUTHORITY.to_owned())
199    } else {
200        Ok(())
201    }
202}
203
204fn normalize_replacement(
205    actor: UserId,
206    mut editors: Vec<Authority>,
207    mut viewers: Vec<ViewerSubject>,
208) -> Result<(Vec<Authority>, Vec<ViewerSubject>), String> {
209    AccessPolicy::new(Authority::User(actor), editors.clone(), viewers.clone())?;
210    editors.sort_unstable();
211    editors.dedup();
212    viewers.sort_unstable();
213    viewers.dedup();
214    viewers.retain(|viewer| match viewer {
215        ViewerSubject::User(user) => editors.binary_search(&Authority::User(*user)).is_err(),
216        ViewerSubject::Group(group) => editors.binary_search(&Authority::Group(*group)).is_err(),
217        ViewerSubject::Model(_) => true,
218    });
219    Ok((editors, viewers))
220}
221
222fn require_group(groups: &[GroupId], group: GroupId) -> Result<(), String> {
223    if groups.contains(&group) {
224        Ok(())
225    } else {
226        Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    fn txid(byte: u8) -> TxId {
235        TxId::from_bytes([byte; 12])
236    }
237
238    fn user(byte: u8) -> UserId {
239        UserId::from_tx_id(txid(byte))
240    }
241
242    fn group(byte: u8) -> GroupId {
243        GroupId::new(txid(byte))
244    }
245
246    fn model(byte: u8) -> ModelId {
247        ModelId::from_bytes([byte; 32])
248    }
249
250    #[test]
251    fn filtered_create_authority_is_rejected() {
252        let authority = Authority::Group(group(7));
253        let context = AccessContext::new(
254            user(1),
255            model(2),
256            FilteredAuthorities::new(vec![authority]).unwrap(),
257        )
258        .unwrap();
259        let policy = AccessPolicy::new(authority, Vec::new(), Vec::new()).unwrap();
260        assert_eq!(
261            require_visible_authority(&context, &policy),
262            Err(FILTERED_AUTHORITY.to_owned())
263        );
264    }
265
266    #[test]
267    fn replacement_lists_are_canonical_and_editor_viewers_are_removed() {
268        let editor_user = user(3);
269        let editor_group = group(4);
270        let visible_model = model(5);
271        let (editors, viewers) = normalize_replacement(
272            user(1),
273            vec![
274                Authority::Group(editor_group),
275                Authority::User(editor_user),
276                Authority::Group(editor_group),
277            ],
278            vec![
279                ViewerSubject::Model(visible_model),
280                ViewerSubject::User(editor_user),
281                ViewerSubject::Group(editor_group),
282                ViewerSubject::Model(visible_model),
283            ],
284        )
285        .unwrap();
286        assert_eq!(
287            editors,
288            vec![Authority::User(editor_user), Authority::Group(editor_group)]
289        );
290        assert_eq!(viewers, vec![ViewerSubject::Model(visible_model)]);
291    }
292
293    #[test]
294    fn replacement_rejects_sentinel_editors_and_all_models_member() {
295        assert!(
296            normalize_replacement(user(1), vec![Authority::Group(ALL_USERS)], Vec::new()).is_err()
297        );
298        assert!(
299            normalize_replacement(
300                user(1),
301                Vec::new(),
302                vec![ViewerSubject::Model(ALL_MODELS_MEMBER)]
303            )
304            .is_err()
305        );
306    }
307
308    #[test]
309    fn requested_group_requires_current_membership() {
310        let requested = group(1);
311        assert_eq!(require_group(&[requested], requested), Ok(()));
312        assert_eq!(
313            require_group(&[group(2), group(3)], requested),
314            Err(ACCESS_GROUP_UNAVAILABLE.to_owned())
315        );
316    }
317}