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