Skip to main content

kcode_k1_access_testkit/
lib.rs

1use std::{
2    path::Path,
3    sync::{Arc, Barrier},
4    thread,
5};
6
7use kcode_k1_access::{
8    AccessCheck, AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities, K1Access,
9    ModelId, ProfileId, SubsystemId, Target, TxId, UserId, ViewerSubject,
10};
11use kcode_k1_groups::K1Groups;
12use kcode_k1_peering::K1Peering;
13use kcode_k1_txn_ordering::K1TxnOrdering;
14use tempfile::TempDir;
15
16struct Stack {
17    _ordering: Arc<K1TxnOrdering>,
18    peering: Arc<K1Peering>,
19    _groups: Arc<K1Groups>,
20    access: Arc<K1Access>,
21}
22
23impl Stack {
24    fn open(root: &Path) -> Result<Self, String> {
25        let ordering = Arc::new(
26            K1TxnOrdering::open(&root.join("ordering"))
27                .map_err(|error| format!("open ordering: {error}"))?,
28        );
29        let peering = Arc::new(
30            K1Peering::open(&root.join("peering"), ordering.clone())
31                .map_err(|error| format!("open peering: {error}"))?,
32        );
33        let groups = Arc::new(
34            K1Groups::open(&root.join("groups"), ordering.clone(), peering.clone())
35                .map_err(|error| format!("open groups: {error}"))?,
36        );
37        let access = Arc::new(
38            K1Access::open(
39                &root.join("access"),
40                ordering.clone(),
41                peering.clone(),
42                groups.clone(),
43            )
44            .map_err(|error| format!("open access: {error}"))?,
45        );
46        Ok(Self {
47            _ordering: ordering,
48            peering,
49            _groups: groups,
50            access,
51        })
52    }
53}
54
55fn temporary_root(label: &str) -> Result<TempDir, String> {
56    tempfile::Builder::new()
57        .prefix(&format!("k1-access-testkit-{label}-"))
58        .tempdir()
59        .map_err(|error| format!("{label}: create temporary root: {error}"))
60}
61
62fn require(condition: bool, label: &str) -> Result<(), String> {
63    if condition {
64        Ok(())
65    } else {
66        Err(label.to_owned())
67    }
68}
69
70fn tx(byte: u8) -> TxId {
71    TxId::from_bytes([byte; 12])
72}
73fn user(byte: u8) -> UserId {
74    UserId::from_tx_id(tx(byte))
75}
76fn profile(byte: u8) -> ProfileId {
77    ProfileId::new(tx(byte))
78}
79fn model(byte: u8) -> ModelId {
80    ModelId::from_bytes([byte; 32])
81}
82
83fn context(user: UserId, model: ModelId) -> Result<AccessContext, String> {
84    AccessContext::new(user, model, FilteredAuthorities::empty())
85}
86
87fn policy(
88    authority: UserId,
89    editor: UserId,
90    viewer: UserId,
91    viewer_model: ModelId,
92) -> Result<AccessPolicy, String> {
93    AccessPolicy::new(
94        Authority::User(authority),
95        vec![Authority::User(editor)],
96        vec![
97            ViewerSubject::User(viewer),
98            ViewerSubject::Model(viewer_model),
99        ],
100    )
101}
102
103fn assert_profile(
104    access: &K1Access,
105    access_id: AccessId,
106    expected: ProfileId,
107    label: &str,
108) -> Result<(), String> {
109    require(access.profile_id(access_id)? == Some(expected), label)
110}
111
112fn assert_check(
113    access: &K1Access,
114    context: &AccessContext,
115    access_id: AccessId,
116    subsystem: SubsystemId,
117    target: &Target,
118    expected: (bool, bool),
119    label: &str,
120) -> Result<AccessCheck, String> {
121    let check = access
122        .check(context, access_id, subsystem)
123        .map_err(|error| format!("{label}: {error}"))?;
124    let target_matches = if expected.0 {
125        check.target() == Some(target)
126    } else {
127        check.target().is_none()
128    };
129    require(
130        (check.can_view(), check.can_edit()) == expected && target_matches,
131        label,
132    )?;
133    Ok(check)
134}
135
136fn assert_hidden(check: &AccessCheck, label: &str) -> Result<(), String> {
137    require(
138        !check.can_view()
139            && !check.can_edit()
140            && check.target().is_none()
141            && check.access_revision().is_none(),
142        label,
143    )
144}
145
146fn assert_list(
147    access: &K1Access,
148    context: &AccessContext,
149    subsystem: SubsystemId,
150    expected: &[AccessId],
151    label: &str,
152) -> Result<(), String> {
153    let actual = access
154        .list_user(context, subsystem)
155        .map_err(|error| format!("{label}: {error}"))?;
156    require(actual == expected, label)
157}
158
159pub fn verify() -> Result<(), String> {
160    kcode_k1_access_groups_testkit::verify()
161        .map_err(|error| format!("Access Groups verifier: {error}"))?;
162    kcode_k1_access_store_testkit::verify_access_store()
163        .map_err(|error| format!("Access store verifier: {error}"))?;
164    kcode_k1_access_privacy_testkit::verify_access_privacy()
165        .map_err(|error| format!("Access privacy verifier: {error}"))?;
166    {
167        let root = temporary_root("lifecycle")?;
168        lifecycle(root.path()).map_err(|error| format!("teardown/reopen scenario: {error}"))?;
169    }
170    {
171        let root = temporary_root("concurrent")?;
172        concurrent(root.path()).map_err(|error| format!("concurrent scenario: {error}"))?;
173    }
174    {
175        let root = temporary_root("malformed")?;
176        malformed_replay(root.path()).map_err(|error| format!("malformed scenario: {error}"))?;
177    }
178    Ok(())
179}
180
181fn lifecycle(root: &Path) -> Result<(), String> {
182    let stack = Stack::open(root)?;
183    let authority = user(1);
184    let old_editor = user(2);
185    let old_viewer = user(3);
186    let new_editor = user(4);
187    let new_viewer = user(5);
188    let second_editor = user(6);
189    let second_viewer = user(7);
190    let old_model = model(11);
191    let new_model = model(12);
192    let second_model = model(13);
193    let first_profile = profile(14);
194    let second_profile = profile(15);
195    let subsystem = SubsystemId::from_str("audio-fragment")?;
196    let first_target = Target::new(subsystem, vec![1, 0, 1]);
197    let second_target = Target::new(subsystem, vec![2, 0, 2]);
198    let owner = context(authority, old_model)?;
199    let first_revision = stack.access.create(
200        &owner,
201        first_target.clone(),
202        first_profile,
203        policy(authority, old_editor, old_viewer, old_model)?,
204    )?;
205    let first_id = first_revision.access_id();
206    assert_profile(
207        &stack.access,
208        first_id,
209        first_profile,
210        "created first profile link",
211    )?;
212    let second_revision = stack.access.create(
213        &context(authority, second_model)?,
214        second_target.clone(),
215        second_profile,
216        policy(authority, second_editor, second_viewer, second_model)?,
217    )?;
218    let second_id = second_revision.access_id();
219    assert_profile(
220        &stack.access,
221        second_id,
222        second_profile,
223        "created second profile link",
224    )?;
225    let initial_editor = context(old_editor, old_model)?;
226    let initial_viewer = context(old_viewer, old_model)?;
227    for (context, label) in [
228        (&initial_editor, "initial editor discovery"),
229        (&initial_viewer, "initial viewer discovery"),
230    ] {
231        assert_list(&stack.access, context, subsystem, &[first_id], label)?;
232    }
233    let replacement = stack.access.replace_policy(
234        &context(authority, new_model)?,
235        first_id,
236        vec![Authority::User(new_editor)],
237        vec![
238            ViewerSubject::User(new_viewer),
239            ViewerSubject::Model(new_model),
240        ],
241    )?;
242    require(
243        replacement.access_id() == first_id && replacement.txid() != first_revision.txid(),
244        "replacement did not retain identity with a new revision",
245    )?;
246    assert_profile(
247        &stack.access,
248        first_id,
249        first_profile,
250        "replacement changed first profile link",
251    )?;
252    let owner_current = context(authority, new_model)?;
253    let old_editor_current = context(old_editor, new_model)?;
254    let old_viewer_current = context(old_viewer, new_model)?;
255    let new_editor_current = context(new_editor, new_model)?;
256    let new_viewer_current = context(new_viewer, new_model)?;
257    for (context, label) in [
258        (&old_editor_current, "removed editor discovery"),
259        (&old_viewer_current, "removed viewer discovery"),
260        (&new_editor_current, "added editor discovery"),
261        (&new_viewer_current, "added viewer discovery"),
262    ] {
263        assert_list(&stack.access, context, subsystem, &[first_id], label)?;
264    }
265    drop(stack);
266    let stack = Stack::open(root)?;
267    assert_profile(
268        &stack.access,
269        first_id,
270        first_profile,
271        "reopened first profile link",
272    )?;
273    assert_profile(
274        &stack.access,
275        second_id,
276        second_profile,
277        "reopened second profile link",
278    )?;
279    for (context, expected, label) in [
280        (&owner_current, (true, true), "reopened authority rights"),
281        (&new_editor_current, (true, true), "reopened editor rights"),
282        (&new_viewer_current, (true, false), "reopened viewer rights"),
283        (
284            &old_editor_current,
285            (false, false),
286            "reopened removed editor",
287        ),
288        (
289            &old_viewer_current,
290            (false, false),
291            "reopened removed viewer",
292        ),
293    ] {
294        let check = assert_check(
295            &stack.access,
296            context,
297            first_id,
298            subsystem,
299            &first_target,
300            expected,
301            label,
302        )?;
303        require(
304            check.access_revision() == expected.0.then_some(replacement.txid()),
305            label,
306        )?;
307    }
308    for (context, expected, label) in [
309        (
310            &owner_current,
311            &[first_id, second_id][..],
312            "reopened authority discovery order",
313        ),
314        (
315            &old_editor_current,
316            &[first_id][..],
317            "reopened removed editor discovery",
318        ),
319        (
320            &old_viewer_current,
321            &[first_id][..],
322            "reopened removed viewer discovery",
323        ),
324        (
325            &new_editor_current,
326            &[first_id][..],
327            "reopened added editor discovery",
328        ),
329        (
330            &new_viewer_current,
331            &[first_id][..],
332            "reopened added viewer discovery",
333        ),
334    ] {
335        assert_list(&stack.access, context, subsystem, expected, label)?;
336    }
337    let filtered = AccessContext::new(
338        new_editor,
339        new_model,
340        FilteredAuthorities::new(vec![Authority::User(authority)])?,
341    )?;
342    let hidden = assert_check(
343        &stack.access,
344        &filtered,
345        first_id,
346        subsystem,
347        &first_target,
348        (false, false),
349        "filtered scalar check",
350    )?;
351    assert_hidden(&hidden, "filtered scalar exposed authority data")?;
352    assert_list(
353        &stack.access,
354        &filtered,
355        subsystem,
356        &[],
357        "filtered list exposed authority data",
358    )?;
359    require(
360        stack
361            .access
362            .list_user_group_targets(&filtered, subsystem)?
363            .is_empty(),
364        "filtered combined metadata exposed authority data",
365    )?;
366    require(
367        stack.access.resolve_visible_targets(
368            &filtered,
369            &[first_target, second_target],
370            subsystem,
371        )? == [None, None],
372        "filtered reverse lookup exposed authority data",
373    )
374}
375
376fn concurrent(root: &Path) -> Result<(), String> {
377    let stack = Stack::open(root)?;
378    let authority = user(21);
379    let a_editor = user(22);
380    let a_viewer = user(23);
381    let b_old_editor = user(24);
382    let b_old_viewer = user(25);
383    let b_new_editor = user(26);
384    let b_new_viewer = user(27);
385    let a_model = model(31);
386    let b_old_model = model(32);
387    let b_new_model = model(33);
388    let a_profile = profile(34);
389    let b_profile = profile(35);
390    let subsystem = SubsystemId::from_str("audio-fragment")?;
391    let target_a = Target::new(subsystem, vec![10, 0, 10]);
392    let target_b = Target::new(subsystem, vec![20, 0, 20]);
393    let a_revision = stack.access.create(
394        &context(authority, a_model)?,
395        target_a.clone(),
396        a_profile,
397        policy(authority, a_editor, a_viewer, a_model)?,
398    )?;
399    let a_id = a_revision.access_id();
400    assert_profile(
401        &stack.access,
402        a_id,
403        a_profile,
404        "created concurrent A profile link",
405    )?;
406    let b_revision = stack.access.create(
407        &context(authority, b_old_model)?,
408        target_b.clone(),
409        b_profile,
410        policy(authority, b_old_editor, b_old_viewer, b_old_model)?,
411    )?;
412    let b_id = b_revision.access_id();
413    assert_profile(
414        &stack.access,
415        b_id,
416        b_profile,
417        "created concurrent B profile link",
418    )?;
419    let barrier = Arc::new(Barrier::new(3));
420    let check_access = stack.access.clone();
421    let check_barrier = barrier.clone();
422    let check_context = context(a_viewer, a_model)?;
423    let replace_access = stack.access.clone();
424    let replace_barrier = barrier.clone();
425    let replace_context = context(authority, b_new_model)?;
426    let (checked, replaced) = thread::scope(|scope| {
427        let checked = scope.spawn(move || {
428            check_barrier.wait();
429            check_access.check(&check_context, a_id, subsystem)
430        });
431        let replaced = scope.spawn(move || {
432            replace_barrier.wait();
433            replace_access.replace_policy(
434                &replace_context,
435                b_id,
436                vec![Authority::User(b_new_editor)],
437                vec![
438                    ViewerSubject::User(b_new_viewer),
439                    ViewerSubject::Model(b_new_model),
440                ],
441            )
442        });
443        barrier.wait();
444        let checked = checked
445            .join()
446            .map_err(|_| "concurrent object A check panicked".to_owned())?
447            .map_err(|error| format!("concurrent object A check: {error}"))?;
448        let replaced = replaced
449            .join()
450            .map_err(|_| "concurrent object B replacement panicked".to_owned())?
451            .map_err(|error| format!("concurrent object B replacement: {error}"))?;
452        Ok::<_, String>((checked, replaced))
453    })?;
454    require(
455        checked.can_view()
456            && !checked.can_edit()
457            && checked.target() == Some(&target_a)
458            && checked.access_revision() == Some(a_revision.txid()),
459        "concurrent object A check returned the wrong result",
460    )?;
461    require(
462        replaced.access_id() == b_id && replaced.txid() != b_revision.txid(),
463        "concurrent object B replacement was not genuine",
464    )?;
465    assert_profile(
466        &stack.access,
467        a_id,
468        a_profile,
469        "concurrent A profile link changed",
470    )?;
471    assert_profile(
472        &stack.access,
473        b_id,
474        b_profile,
475        "concurrent B profile link changed",
476    )?;
477    let b_new_editor_context = context(b_new_editor, b_new_model)?;
478    let b_new_viewer_context = context(b_new_viewer, b_new_model)?;
479    let b_old_editor_context = context(b_old_editor, b_new_model)?;
480    let b_old_viewer_context = context(b_old_viewer, b_new_model)?;
481    for (context, expected, label) in [
482        (
483            &b_new_editor_context,
484            (true, true),
485            "changed B editor rights",
486        ),
487        (
488            &b_new_viewer_context,
489            (true, false),
490            "changed B viewer rights",
491        ),
492        (
493            &b_old_editor_context,
494            (false, false),
495            "removed B editor rights",
496        ),
497        (
498            &b_old_viewer_context,
499            (false, false),
500            "removed B viewer rights",
501        ),
502    ] {
503        let check = assert_check(
504            &stack.access,
505            context,
506            b_id,
507            subsystem,
508            &target_b,
509            expected,
510            label,
511        )?;
512        require(
513            check.access_revision() == expected.0.then_some(replaced.txid()),
514            label,
515        )?;
516    }
517    Ok(())
518}
519
520fn malformed_replay(root: &Path) -> Result<(), String> {
521    let stack = Stack::open(root)?;
522    let subsystem = SubsystemId::from_str("k1-access-subsystem")?;
523    let error = match stack.peering.submit_txn(subsystem, &[255]) {
524        Ok(_) => return Err("malformed callback unexpectedly succeeded".to_owned()),
525        Err(error) => error,
526    };
527    require(
528        error.to_ascii_lowercase().contains("committed"),
529        "malformed callback was not committed before failure",
530    )?;
531    require(
532        stack
533            .access
534            .check(
535                &context(user(61), model(61))?,
536                AccessId::new(tx(61)),
537                subsystem,
538            )
539            .is_err(),
540        "malformed callback left the live Access handle available",
541    )?;
542    drop(stack);
543    match Stack::open(root) {
544        Ok(_) => Err("fresh stack open replayed committed malformed input".to_owned()),
545        Err(_) => Ok(()),
546    }
547}
548
549#[cfg(test)]
550mod tests {
551    #[test]
552    fn full_stack_conformance() {
553        super::verify().unwrap();
554    }
555}