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}