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", ¤t_viewer, (true, false)),
325 ("replacement editor", ¤t_editor, (false, true)),
326 ("immutable authority", ¤t_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}