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}