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