Skip to main content

kcode_k1_access_privacy_testkit/
lib.rs

1use std::{fmt::Debug, sync::Arc};
2
3use kcode_k1_access::{
4    ALL_USERS, AccessCheck, AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities,
5    GroupId, K1Access, ModelId, ProfileId, SubsystemId, Target, TxId, UserId, ViewerSubject,
6};
7use kcode_k1_groups::K1Groups;
8use kcode_k1_peering::K1Peering;
9use kcode_k1_txn_ordering::K1TxnOrdering;
10
11fn require(condition: bool, label: &str) -> Result<(), String> {
12    if condition {
13        Ok(())
14    } else {
15        Err(label.to_owned())
16    }
17}
18
19fn equal<T>(actual: &T, expected: &T, label: &str) -> Result<(), String>
20where
21    T: Debug + PartialEq + ?Sized,
22{
23    if actual == expected {
24        Ok(())
25    } else {
26        Err(format!(
27            "{label}: expected {expected:?}, received {actual:?}"
28        ))
29    }
30}
31
32fn failure<T>(result: Result<T, String>, label: &str) -> Result<(), String> {
33    match result {
34        Err(_) => Ok(()),
35        Ok(_) => Err(format!("{label}: operation unexpectedly succeeded")),
36    }
37}
38
39fn exact_failure<T>(result: Result<T, String>, expected: &str, label: &str) -> Result<(), String> {
40    match result {
41        Err(error) => equal(error.as_str(), expected, label),
42        Ok(_) => Err(format!("{label}: operation unexpectedly succeeded")),
43    }
44}
45
46fn txid(byte: u8) -> TxId {
47    TxId::from_bytes([byte; 12])
48}
49
50fn profile(byte: u8) -> ProfileId {
51    ProfileId::new(txid(byte))
52}
53
54fn user(byte: u8) -> UserId {
55    UserId::from_tx_id(txid(byte))
56}
57
58fn model(byte: u8) -> ModelId {
59    ModelId::from_bytes([byte; 32])
60}
61
62fn context(
63    user: UserId,
64    model: ModelId,
65    filtered: Vec<Authority>,
66) -> Result<AccessContext, String> {
67    AccessContext::new(user, model, FilteredAuthorities::new(filtered)?)
68}
69
70fn expect_check(
71    check: &AccessCheck,
72    target: &Target,
73    (view, edit): (bool, bool),
74    label: &str,
75) -> Result<(), String> {
76    equal(&check.can_view(), &view, &format!("{label} view"))?;
77    equal(&check.can_edit(), &edit, &format!("{label} edit"))?;
78    let expected_target = view.then_some(target);
79    equal(
80        &check.target(),
81        &expected_target,
82        &format!("{label} target"),
83    )
84}
85
86fn expect_rights(
87    access: &K1Access,
88    context: &AccessContext,
89    access_id: AccessId,
90    subsystem: SubsystemId,
91    target: &Target,
92    rights: (bool, bool),
93    label: &str,
94) -> Result<(), String> {
95    expect_check(
96        &access.check(context, access_id, subsystem)?,
97        target,
98        rights,
99        label,
100    )
101}
102
103fn expect_discovery(
104    access: &K1Access,
105    context: &AccessContext,
106    group: Option<GroupId>,
107    subsystem: SubsystemId,
108    access_id: AccessId,
109    expected: usize,
110    label: &str,
111) -> Result<(), String> {
112    let discovered = match group {
113        Some(group) => access.list_group(context, group, subsystem)?,
114        None => access.list_user(context, subsystem)?,
115    };
116    equal(
117        &discovered
118            .iter()
119            .filter(|candidate| **candidate == access_id)
120            .count(),
121        &expected,
122        label,
123    )
124}
125
126pub fn verify_access_privacy() -> Result<(), String> {
127    let root = tempfile::tempdir().map_err(|error| format!("create temporary root: {error}"))?;
128    let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering"))?);
129    let peering = Arc::new(K1Peering::open(
130        &root.path().join("peering"),
131        Arc::clone(&ordering),
132    )?);
133    let groups = Arc::new(K1Groups::open(
134        &root.path().join("groups"),
135        Arc::clone(&ordering),
136        Arc::clone(&peering),
137    )?);
138    let access = K1Access::open(
139        &root.path().join("access"),
140        Arc::clone(&ordering),
141        Arc::clone(&peering),
142        groups,
143    )?;
144
145    let subsystem = SubsystemId::from_str("privacy")?;
146    let wrong_subsystem = SubsystemId::from_str("other")?;
147    let owner = user(1);
148    let editor = user(2);
149    let viewer = user(3);
150    let outsider = user(4);
151    let new_editor = user(5);
152    let new_viewer = user(6);
153    let second_owner = user(7);
154    let allowed_model = model(1);
155    let hidden_model = model(2);
156    let replacement_model = model(3);
157    let main_target = Target::new(subsystem, vec![1]);
158    let rejected_profile = profile(9);
159    let main_profile = profile(10);
160    let repair_profile = profile(11);
161    let wrong_profile = profile(12);
162
163    let initial_policy = AccessPolicy::new(
164        Authority::User(owner),
165        vec![Authority::User(editor)],
166        vec![
167            ViewerSubject::User(viewer),
168            ViewerSubject::Model(allowed_model),
169        ],
170    )?;
171    equal(
172        &initial_policy.authority(),
173        &Authority::User(owner),
174        "singular user authority",
175    )?;
176    equal(
177        initial_policy.editors(),
178        &[Authority::User(editor)],
179        "direct editor",
180    )?;
181    equal(
182        initial_policy.viewers(),
183        &[
184            ViewerSubject::User(viewer),
185            ViewerSubject::Model(allowed_model),
186        ],
187        "direct user and model viewers",
188    )?;
189
190    let authority_filtered = context(viewer, allowed_model, vec![Authority::User(owner)])?;
191    failure(
192        access.create(
193            &authority_filtered,
194            main_target.clone(),
195            rejected_profile,
196            initial_policy.clone(),
197        ),
198        "create with filtered chosen authority",
199    )?;
200
201    let owner_visible = context(owner, allowed_model, vec![])?;
202    let created = access.create(
203        &owner_visible,
204        main_target.clone(),
205        main_profile,
206        initial_policy,
207    )?;
208    let main_id = created.access_id();
209    equal(
210        &access.profile_id(main_id)?,
211        &Some(main_profile),
212        "main profile link",
213    )?;
214    let owner_hidden = context(owner, hidden_model, vec![])?;
215    let editor_hidden = context(editor, hidden_model, vec![])?;
216    let viewer_visible = context(viewer, allowed_model, vec![])?;
217    let viewer_hidden = context(viewer, hidden_model, vec![])?;
218    let outsider_visible = context(outsider, allowed_model, vec![])?;
219    for (label, fixture, rights) in [
220        ("authority without model view", &owner_hidden, (false, true)),
221        ("editor without model view", &editor_hidden, (false, true)),
222        ("dual user and model gate", &viewer_visible, (true, false)),
223        ("missing model gate", &viewer_hidden, (false, false)),
224        ("model never edits", &outsider_visible, (false, false)),
225    ] {
226        expect_rights(
227            &access,
228            fixture,
229            main_id,
230            subsystem,
231            &main_target,
232            rights,
233            label,
234        )?;
235    }
236
237    let concealed = AccessCheck::new(false, false, None, None, None)?;
238    equal(
239        &access.check(&authority_filtered, main_id, subsystem)?,
240        &concealed,
241        "filtered check concealment",
242    )?;
243    equal(
244        &access.list_user(&authority_filtered, subsystem)?,
245        &Vec::<AccessId>::new(),
246        "filtered list concealment",
247    )?;
248    require(
249        access
250            .list_user_group_targets(&authority_filtered, subsystem)?
251            .is_empty(),
252        "filtered combined metadata was visible",
253    )?;
254    equal(
255        &access.resolve_visible_targets(
256            &authority_filtered,
257            std::slice::from_ref(&main_target),
258            subsystem,
259        )?,
260        &vec![None],
261        "filtered reverse concealment",
262    )?;
263
264    let ordinary_group = GroupId::new(txid(90));
265    require(
266        ordinary_group.sentinel().is_none(),
267        "ordinary group fixture used a sentinel",
268    )?;
269    let unrelated_group_filter =
270        context(owner, allowed_model, vec![Authority::Group(ordinary_group)])?;
271    expect_rights(
272        &access,
273        &unrelated_group_filter,
274        main_id,
275        subsystem,
276        &main_target,
277        (true, true),
278        "ordinary group filter hid personal authority",
279    )?;
280
281    let editor_filtered = context(editor, allowed_model, vec![Authority::User(owner)])?;
282    let new_editors = vec![Authority::User(new_editor)];
283    let new_viewers = vec![
284        ViewerSubject::User(new_viewer),
285        ViewerSubject::Model(replacement_model),
286    ];
287    exact_failure(
288        access.replace_policy(
289            &editor_filtered,
290            main_id,
291            new_editors.clone(),
292            new_viewers.clone(),
293        ),
294        "access is unavailable",
295        "filtered replacement",
296    )?;
297    let replaced = access.replace_policy(&editor_hidden, main_id, new_editors, new_viewers)?;
298    equal(
299        &replaced.access_id(),
300        &main_id,
301        "replacement access identity",
302    )?;
303    equal(
304        &access.profile_id(main_id)?,
305        &Some(main_profile),
306        "policy replacement preserves main profile link",
307    )?;
308    require(
309        replaced.txid() != created.txid(),
310        "replacement did not advance the revision",
311    )?;
312
313    let old_editor = context(editor, replacement_model, vec![])?;
314    let old_viewer = context(viewer, replacement_model, vec![])?;
315    let removed_model = context(new_viewer, allowed_model, vec![])?;
316    let current_viewer = context(new_viewer, replacement_model, vec![])?;
317    let current_editor = context(new_editor, hidden_model, vec![])?;
318    let current_owner = context(owner, replacement_model, vec![])?;
319    let model_only = context(outsider, replacement_model, vec![])?;
320    for (label, fixture, rights) in [
321        ("removed editor", &old_editor, (false, false)),
322        ("removed user viewer", &old_viewer, (false, false)),
323        ("removed model viewer", &removed_model, (false, false)),
324        ("replacement viewer", &current_viewer, (true, false)),
325        ("replacement editor", &current_editor, (false, true)),
326        ("immutable authority", &current_owner, (true, true)),
327        ("replacement model never edits", &model_only, (false, false)),
328    ] {
329        expect_rights(
330            &access,
331            fixture,
332            main_id,
333            subsystem,
334            &main_target,
335            rights,
336            label,
337        )?;
338    }
339    for (label, discovered_user) in [
340        ("authority discovery", owner),
341        ("old editor discovery", editor),
342        ("old viewer discovery", viewer),
343        ("new editor discovery", new_editor),
344        ("new viewer discovery", new_viewer),
345    ] {
346        expect_discovery(
347            &access,
348            &context(discovered_user, hidden_model, vec![])?,
349            None,
350            subsystem,
351            main_id,
352            1,
353            label,
354        )?;
355    }
356
357    let repair_target = Target::new(subsystem, vec![2]);
358    let group_policy = AccessPolicy::new(
359        Authority::User(second_owner),
360        vec![],
361        vec![
362            ViewerSubject::Group(ALL_USERS),
363            ViewerSubject::Model(replacement_model),
364        ],
365    )?;
366    let repair_id = access
367        .create(
368            &owner_visible,
369            repair_target.clone(),
370            repair_profile,
371            group_policy.clone(),
372        )?
373        .access_id();
374    equal(
375        &access.profile_id(repair_id)?,
376        &Some(repair_profile),
377        "repair profile link",
378    )?;
379    let repair_context = context(new_viewer, replacement_model, vec![])?;
380    expect_discovery(
381        &access,
382        &repair_context,
383        None,
384        subsystem,
385        repair_id,
386        0,
387        "group-backed discovery leaves personal list empty before scalar check",
388    )?;
389    expect_discovery(
390        &access,
391        &repair_context,
392        Some(ALL_USERS),
393        subsystem,
394        repair_id,
395        1,
396        "ALL_USERS group discovery exists before scalar check",
397    )?;
398    let repaired = access.check(&repair_context, repair_id, subsystem)?;
399    expect_check(
400        &repaired,
401        &repair_target,
402        (true, false),
403        "group-backed scalar check grants view",
404    )?;
405    equal(
406        &access.check(&repair_context, repair_id, subsystem)?,
407        &repaired,
408        "repeat group-backed scalar check",
409    )?;
410    expect_discovery(
411        &access,
412        &repair_context,
413        None,
414        subsystem,
415        repair_id,
416        0,
417        "group-backed scalar checks do not copy discovery to personal list",
418    )?;
419    expect_discovery(
420        &access,
421        &repair_context,
422        Some(ALL_USERS),
423        subsystem,
424        repair_id,
425        1,
426        "group-backed scalar checks do not duplicate ALL_USERS discovery",
427    )?;
428
429    let wrong_target = Target::new(wrong_subsystem, vec![3]);
430    let wrong_id = access
431        .create(
432            &owner_visible,
433            wrong_target.clone(),
434            wrong_profile,
435            group_policy,
436        )?
437        .access_id();
438    equal(
439        &access.profile_id(wrong_id)?,
440        &Some(wrong_profile),
441        "wrong-subsystem profile link",
442    )?;
443    let batch_context = context(new_viewer, replacement_model, vec![Authority::User(owner)])?;
444    let unknown_id = AccessId::new(txid(240));
445    let visible = access.check(&batch_context, repair_id, subsystem)?;
446    equal(
447        &access.check_many(
448            &batch_context,
449            &[repair_id, unknown_id, repair_id, main_id, wrong_id],
450            subsystem,
451        )?,
452        &vec![
453            visible.clone(),
454            concealed.clone(),
455            visible,
456            concealed.clone(),
457            concealed.clone(),
458        ],
459        "ordered batch concealment",
460    )?;
461
462    let unknown_target = Target::new(subsystem, vec![240]);
463    equal(
464        &access.resolve_visible_targets(
465            &batch_context,
466            &[
467                repair_target.clone(),
468                unknown_target,
469                repair_target,
470                main_target,
471                wrong_target,
472            ],
473            subsystem,
474        )?,
475        &vec![Some(repair_id), None, Some(repair_id), None, None],
476        "ordered reverse concealment",
477    )?;
478    equal(
479        &access.check_many(&batch_context, &[], subsystem)?,
480        &Vec::<AccessCheck>::new(),
481        "empty batch",
482    )?;
483    equal(
484        &access.resolve_visible_targets(&batch_context, &[], subsystem)?,
485        &Vec::<Option<AccessId>>::new(),
486        "empty reverse",
487    )?;
488
489    Ok(())
490}
491
492#[cfg(test)]
493mod tests {
494    #[test]
495    fn verifies_access_privacy() {
496        super::verify_access_privacy().unwrap();
497    }
498}