Skip to main content

kcode_k1_access_groups_testkit/
lib.rs

1use std::sync::Arc;
2
3use kcode_k1_access::{
4    AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities, K1Access, ModelId,
5    SubsystemId, Target, TxId, UserId, ViewerSubject,
6};
7use kcode_k1_groups::{ALL_MODELS, ALL_USERS, GroupId, GroupName, GroupRole, K1Groups};
8use kcode_k1_peering::K1Peering;
9use kcode_k1_txn_ordering::K1TxnOrdering;
10
11struct Stack {
12    _ordering: Arc<K1TxnOrdering>,
13    _peering: Arc<K1Peering>,
14    groups: Arc<K1Groups>,
15    access: Arc<K1Access>,
16}
17
18impl Stack {
19    fn open(root: &std::path::Path) -> Result<Self, String> {
20        let ordering = Arc::new(at(
21            "open ordering",
22            K1TxnOrdering::open(&root.join("ordering")),
23        )?);
24        let peering = Arc::new(at(
25            "open peering",
26            K1Peering::open(&root.join("peering"), ordering.clone()),
27        )?);
28        let groups = Arc::new(at(
29            "open groups",
30            K1Groups::open(&root.join("groups"), ordering.clone(), peering.clone()),
31        )?);
32        let access = Arc::new(at(
33            "open access",
34            K1Access::open(
35                &root.join("access"),
36                ordering.clone(),
37                peering.clone(),
38                groups.clone(),
39            ),
40        )?);
41        Ok(Self {
42            _ordering: ordering,
43            _peering: peering,
44            groups,
45            access,
46        })
47    }
48
49    fn group(&self, owner: UserId, label: &str) -> Result<GroupId, String> {
50        let name = GroupName::new(label.to_owned())?;
51        Ok(at(label, self.groups.create(owner, name))?.group_id())
52    }
53
54    fn role(
55        &self,
56        owner: UserId,
57        group: GroupId,
58        member: UserId,
59        value: Option<GroupRole>,
60    ) -> Result<(), String> {
61        at(
62            "set user role",
63            self.groups.set_user_role(owner, group, member, value),
64        )?;
65        Ok(())
66    }
67
68    fn model_member(
69        &self,
70        owner: UserId,
71        group: GroupId,
72        model: ModelId,
73        present: bool,
74    ) -> Result<(), String> {
75        at(
76            "set model membership",
77            self.groups
78                .set_model_membership(owner, group, model, present),
79        )?;
80        Ok(())
81    }
82}
83
84fn at<T>(label: &str, result: Result<T, String>) -> Result<T, String> {
85    result.map_err(|error| format!("{label}: {error}"))
86}
87
88fn require(condition: bool, label: &str) -> Result<(), String> {
89    condition.then_some(()).ok_or_else(|| label.to_owned())
90}
91
92fn require_error<T>(result: Result<T, String>, expected: &str, label: &str) -> Result<(), String> {
93    match result {
94        Err(error) if error == expected => Ok(()),
95        _ => Err(label.to_owned()),
96    }
97}
98
99fn user(byte: u8) -> UserId {
100    UserId::from_tx_id(TxId::from_bytes([byte; 12]))
101}
102
103fn model(byte: u8) -> ModelId {
104    ModelId::from_bytes([byte; 32])
105}
106
107fn context(user: UserId, model: ModelId) -> Result<AccessContext, String> {
108    AccessContext::new(user, model, FilteredAuthorities::empty())
109}
110
111fn exact(user: UserId, model: ModelId, group: GroupId) -> Result<AccessContext, String> {
112    AccessContext::new(
113        user,
114        model,
115        FilteredAuthorities::new(vec![Authority::Group(group)])?,
116    )
117}
118
119fn editors(user: UserId, group: GroupId) -> Vec<Authority> {
120    vec![Authority::User(user), Authority::Group(group)]
121}
122
123fn viewers(
124    user: UserId,
125    outsider: UserId,
126    user_group: GroupId,
127    model_group: GroupId,
128    model: ModelId,
129) -> Vec<ViewerSubject> {
130    vec![
131        ViewerSubject::User(user),
132        ViewerSubject::User(outsider),
133        ViewerSubject::Group(user_group),
134        ViewerSubject::Group(model_group),
135        ViewerSubject::Model(model),
136    ]
137}
138
139struct Scenario<'a> {
140    stack: &'a Stack,
141    access: AccessId,
142    subsystem: SubsystemId,
143}
144
145impl Scenario<'_> {
146    fn rights(
147        &self,
148        user: UserId,
149        model: ModelId,
150        view: bool,
151        edit: bool,
152        label: &str,
153    ) -> Result<(), String> {
154        let result = at(
155            label,
156            self.stack
157                .access
158                .check(&context(user, model)?, self.access, self.subsystem),
159        )?;
160        require(
161            result.can_view() == view
162                && result.can_edit() == edit
163                && result.target().is_some() == view,
164            label,
165        )
166    }
167
168    fn group_list(
169        &self,
170        user: UserId,
171        model: ModelId,
172        group: GroupId,
173        expected: &[AccessId],
174        label: &str,
175    ) -> Result<(), String> {
176        let actual = at(
177            label,
178            self.stack
179                .access
180                .list_group(&context(user, model)?, group, self.subsystem),
181        )?;
182        require(actual == expected, label)
183    }
184
185    fn user_list(
186        &self,
187        user: UserId,
188        model: ModelId,
189        expected: &[AccessId],
190        label: &str,
191    ) -> Result<(), String> {
192        let actual = at(
193            label,
194            self.stack
195                .access
196                .list_user(&context(user, model)?, self.subsystem),
197        )?;
198        require(actual == expected, label)
199    }
200
201    fn concealed(&self, context: &AccessContext, label: &str) -> Result<(), String> {
202        let result = at(
203            label,
204            self.stack
205                .access
206                .check(context, self.access, self.subsystem),
207        )?;
208        require(
209            !result.can_view()
210                && !result.can_edit()
211                && result.target().is_none()
212                && result.access_revision().is_none()
213                && result.groups_revision().is_none(),
214            label,
215        )
216    }
217}
218
219pub fn verify() -> Result<(), String> {
220    let root = tempfile::tempdir().map_err(|error| format!("create root: {error}"))?;
221    let stack = Stack::open(root.path())?;
222    let owner = user(1);
223    let member = user(2);
224    let admin = user(3);
225    let group_editor = user(4);
226    let viewer = user(5);
227    let outsider = user(6);
228    let late = user(7);
229    let model_only = user(8);
230    let direct_editor = user(9);
231    let authority_model = model(11);
232    let editor_model = model(12);
233    let viewer_model = model(13);
234    let direct_model = model(14);
235    let no_view_model = model(15);
236    let subsystem = SubsystemId::from_str("audio-fragment")?;
237
238    let authority_group = stack.group(owner, "authority group")?;
239    let editor_group = stack.group(owner, "editor group")?;
240    let user_viewer_group = stack.group(owner, "user viewer group")?;
241    let model_viewer_group = stack.group(owner, "model viewer group")?;
242    let replacement_group = stack.group(owner, "replacement viewer group")?;
243    for (group, member, value) in [
244        (authority_group, member, GroupRole::User),
245        (authority_group, admin, GroupRole::Admin),
246        (editor_group, group_editor, GroupRole::User),
247        (user_viewer_group, viewer, GroupRole::User),
248        (replacement_group, late, GroupRole::User),
249    ] {
250        stack.role(owner, group, member, Some(value))?;
251    }
252    for (group, member) in [
253        (authority_group, authority_model),
254        (editor_group, editor_model),
255        (model_viewer_group, viewer_model),
256    ] {
257        stack.model_member(owner, group, member, true)?;
258    }
259
260    let group_access = at(
261        "create group access",
262        stack.access.create(
263            &context(owner, authority_model)?,
264            Target::new(subsystem, vec![0, 255, 7, 0]),
265            AccessPolicy::new(
266                Authority::Group(authority_group),
267                editors(direct_editor, editor_group),
268                viewers(
269                    member,
270                    outsider,
271                    user_viewer_group,
272                    model_viewer_group,
273                    direct_model,
274                ),
275            )?,
276        ),
277    )?
278    .access_id();
279    let personal_access = at(
280        "create personal access",
281        stack.access.create(
282            &context(member, direct_model)?,
283            Target::new(subsystem, vec![2, 0, 2, 0]),
284            AccessPolicy::new(
285                Authority::User(member),
286                Vec::new(),
287                vec![
288                    ViewerSubject::Group(ALL_USERS),
289                    ViewerSubject::Group(ALL_MODELS),
290                ],
291            )?,
292        ),
293    )?
294    .access_id();
295    let scenario = Scenario {
296        stack: &stack,
297        access: group_access,
298        subsystem,
299    };
300
301    for (user, model, view, edit, label) in [
302        (owner, no_view_model, false, true, "authority owner edit"),
303        (member, no_view_model, false, true, "authority user edit"),
304        (admin, no_view_model, false, true, "authority admin edit"),
305        (
306            outsider,
307            authority_model,
308            true,
309            false,
310            "authority model view",
311        ),
312        (
313            group_editor,
314            no_view_model,
315            false,
316            true,
317            "editor human edit",
318        ),
319        (outsider, editor_model, true, false, "editor model view"),
320        (
321            group_editor,
322            editor_model,
323            true,
324            true,
325            "editor combined rights",
326        ),
327        (viewer, viewer_model, true, false, "split viewer groups"),
328        (viewer, direct_model, true, false, "direct model viewer"),
329        (viewer, no_view_model, false, false, "user-only viewer"),
330        (model_only, direct_model, false, false, "model-only viewer"),
331    ] {
332        scenario.rights(user, model, view, edit, label)?;
333    }
334
335    for (user, group, label) in [
336        (owner, authority_group, "authority discovery"),
337        (group_editor, editor_group, "group editor discovery"),
338        (viewer, user_viewer_group, "user viewer group discovery"),
339        (owner, model_viewer_group, "model viewer group discovery"),
340    ] {
341        scenario.group_list(user, no_view_model, group, &[group_access], label)?;
342    }
343    for (user, expected, label) in [
344        (
345            direct_editor,
346            &[group_access][..],
347            "direct editor discovery",
348        ),
349        (
350            outsider,
351            &[group_access][..],
352            "direct user viewer discovery",
353        ),
354        (
355            member,
356            &[group_access, personal_access][..],
357            "member discovery",
358        ),
359        (model_only, &[][..], "models excluded from discovery"),
360    ] {
361        scenario.user_list(user, direct_model, expected, label)?;
362    }
363    scenario.group_list(
364        model_only,
365        direct_model,
366        ALL_USERS,
367        &[personal_access],
368        "all users discovery",
369    )?;
370
371    let filtered = exact(member, direct_model, authority_group)?;
372    scenario.concealed(&filtered, "filtered check was not concealed")?;
373    let personal = at(
374        "filtered personal authority",
375        stack.access.check(&filtered, personal_access, subsystem),
376    )?;
377    require(
378        personal.can_view() && personal.can_edit() && personal.target().is_some(),
379        "filter hid personal authority",
380    )?;
381    let listed = at(
382        "filtered group listing",
383        stack
384            .access
385            .list_group(&filtered, authority_group, subsystem),
386    )?;
387    require(listed.is_empty(), "filtered group listing was visible")?;
388    let listed = at(
389        "filtered user listing",
390        stack.access.list_user(&filtered, subsystem),
391    )?;
392    require(
393        listed == [personal_access],
394        "filtered user listing was incorrect",
395    )?;
396
397    stack.role(owner, user_viewer_group, late, Some(GroupRole::User))?;
398    scenario.group_list(
399        late,
400        no_view_model,
401        user_viewer_group,
402        &[group_access],
403        "late member discovery",
404    )?;
405    stack.role(owner, user_viewer_group, late, None)?;
406    require_error(
407        stack
408            .access
409            .list_group(&context(late, no_view_model)?, user_viewer_group, subsystem),
410        "access group is unavailable",
411        "former member group listing",
412    )?;
413
414    for (group, user, model, value, label) in [
415        (
416            authority_group,
417            admin,
418            authority_model,
419            GroupRole::Admin,
420            "authority",
421        ),
422        (
423            editor_group,
424            group_editor,
425            editor_model,
426            GroupRole::User,
427            "editor",
428        ),
429        (
430            user_viewer_group,
431            viewer,
432            viewer_model,
433            GroupRole::User,
434            "viewer",
435        ),
436    ] {
437        stack.role(owner, group, user, None)?;
438        scenario.rights(user, model, false, false, label)?;
439        stack.role(owner, group, user, Some(value))?;
440    }
441    for (group, user, model, view, edit, label) in [
442        (
443            authority_group,
444            owner,
445            authority_model,
446            false,
447            true,
448            "authority model",
449        ),
450        (
451            editor_group,
452            group_editor,
453            editor_model,
454            false,
455            true,
456            "editor model",
457        ),
458        (
459            model_viewer_group,
460            viewer,
461            viewer_model,
462            false,
463            false,
464            "viewer model",
465        ),
466    ] {
467        stack.model_member(owner, group, model, false)?;
468        scenario.rights(user, model, view, edit, label)?;
469        stack.model_member(owner, group, model, true)?;
470    }
471
472    scenario.rights(
473        direct_editor,
474        no_view_model,
475        false,
476        true,
477        "editor without model view",
478    )?;
479    require_error(
480        stack.access.replace_policy(
481            &exact(direct_editor, no_view_model, authority_group)?,
482            group_access,
483            editors(direct_editor, editor_group),
484            viewers(
485                member,
486                outsider,
487                replacement_group,
488                model_viewer_group,
489                direct_model,
490            ),
491        ),
492        "access is unavailable",
493        "filtered replacement",
494    )?;
495    at(
496        "replace policy",
497        stack.access.replace_policy(
498            &context(direct_editor, no_view_model)?,
499            group_access,
500            editors(direct_editor, editor_group),
501            viewers(
502                member,
503                outsider,
504                replacement_group,
505                model_viewer_group,
506                direct_model,
507            ),
508        ),
509    )?;
510    scenario.rights(
511        owner,
512        authority_model,
513        true,
514        true,
515        "authority after replacement",
516    )?;
517    scenario.concealed(&filtered, "replacement changed filtered authority")?;
518    scenario.group_list(
519        late,
520        no_view_model,
521        replacement_group,
522        &[group_access],
523        "replacement discovery",
524    )?;
525    scenario.group_list(
526        viewer,
527        no_view_model,
528        user_viewer_group,
529        &[group_access],
530        "monotonic discovery",
531    )
532}
533
534#[cfg(test)]
535mod tests {
536    #[test]
537    fn groups_backed_access_conformance() {
538        super::verify().unwrap();
539    }
540}