1use std::sync::Arc;
2
3use kcode_k1_access::{
4 AccessCheck, AccessId, Authorizations, K1Access, ModelId, OwnerSubject, RequestPrincipal,
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
50fn at<T>(label: &str, result: Result<T, String>) -> Result<T, String> {
51 result.map_err(|error| format!("{label}: {error}"))
52}
53
54fn require(condition: bool, label: &str) -> Result<(), String> {
55 condition.then_some(()).ok_or_else(|| label.to_owned())
56}
57
58fn tx(byte: u8) -> TxId {
59 TxId::from_bytes([byte; 12])
60}
61
62fn user(byte: u8) -> UserId {
63 UserId::from_tx_id(tx(byte))
64}
65
66fn model(byte: u8) -> ModelId {
67 ModelId::from_bytes([byte; 32])
68}
69
70fn name(value: &str) -> Result<GroupName, String> {
71 GroupName::new(value.to_owned())
72}
73
74fn auth(
75 owners: Vec<OwnerSubject>,
76 viewers: Vec<ViewerSubject>,
77 label: &str,
78) -> Result<Authorizations, String> {
79 at(label, Authorizations::new(owners, viewers))
80}
81
82fn check(
83 stack: &Stack,
84 principal: RequestPrincipal,
85 access_id: AccessId,
86 subsystem: SubsystemId,
87 label: &str,
88) -> Result<AccessCheck, String> {
89 at(label, stack.access.check(principal, access_id, subsystem))
90}
91
92fn create_group(stack: &Stack, owner: UserId, label: &str) -> Result<GroupId, String> {
93 Ok(at(label, stack.groups.create(owner, name(label)?))?.group_id())
94}
95
96fn listed(
97 stack: &Stack,
98 principal: RequestPrincipal,
99 group: GroupId,
100 subsystem: SubsystemId,
101 expected: &[AccessId],
102 label: &str,
103) -> Result<(), String> {
104 let actual = at(label, stack.access.list_group(principal, group, subsystem))?;
105 require(actual == expected, label)
106}
107
108pub fn verify() -> Result<(), String> {
109 let root = tempfile::tempdir().map_err(|error| format!("create root: {error}"))?;
110 let stack = Stack::open(root.path())?;
111 let owner = user(1);
112 let member = user(2);
113 let viewer = user(3);
114 let cross_user = user(4);
115 let late_user = user(5);
116 let group_model = model(11);
117 let direct_model = model(12);
118 let cross_model = model(14);
119 let subsystem = SubsystemId::from_str("audio-fragment")?;
120
121 let sentinel_target = Target::new(subsystem, vec![0, 1, 0, 1]);
122 let sentinel_access = at(
123 "create sentinel access",
124 stack.access.create(
125 sentinel_target.clone(),
126 auth(
127 vec![OwnerSubject::User(owner)],
128 vec![
129 ViewerSubject::Group(ALL_USERS),
130 ViewerSubject::Group(ALL_MODELS),
131 ],
132 "sentinel authorizations",
133 )?,
134 ),
135 )?
136 .access_id();
137 let sentinel = check(
138 &stack,
139 RequestPrincipal::new(user(21), model(21)),
140 sentinel_access,
141 subsystem,
142 "sentinel viewer check",
143 )?;
144 require(
145 sentinel.can_view()
146 && !sentinel.can_manage()
147 && sentinel.target() == Some(&sentinel_target),
148 "user/model sentinels did not grant dimensioned view-only access",
149 )?;
150 listed(
151 &stack,
152 RequestPrincipal::new(user(21), model(21)),
153 ALL_USERS,
154 subsystem,
155 &[sentinel_access],
156 "ordinary user lists all-users discovery",
157 )?;
158
159 let current_user = user(22);
160 let built_in_target = Target::new(subsystem, vec![0, 2, 0, 2]);
161 let built_in_access = at(
162 "create built-in-shape access",
163 stack.access.create(
164 built_in_target.clone(),
165 auth(
166 vec![OwnerSubject::User(current_user)],
167 vec![ViewerSubject::Group(ALL_MODELS)],
168 "built-in-shape authorizations",
169 )?,
170 ),
171 )?
172 .access_id();
173 let built_in = check(
174 &stack,
175 RequestPrincipal::new(current_user, model(22)),
176 built_in_access,
177 subsystem,
178 "built-in-shape check",
179 )?;
180 require(
181 built_in.can_view() && built_in.can_manage() && built_in.target() == Some(&built_in_target),
182 "built-in-profile authorization shape did not grant view and management",
183 )?;
184
185 let owner_group = create_group(&stack, owner, "create owner group")?;
186 at(
187 "add owner-group user",
188 stack
189 .groups
190 .set_user_role(owner, owner_group, member, Some(GroupRole::User)),
191 )?;
192 at(
193 "add owner-group model",
194 stack
195 .groups
196 .set_model_membership(owner, owner_group, group_model, true),
197 )?;
198 let target = Target::new(subsystem, vec![0, 255, 7, 0]);
199 let access_id = at(
200 "create group-owned access",
201 stack.access.create(
202 target.clone(),
203 auth(
204 vec![OwnerSubject::Group(owner_group)],
205 vec![
206 ViewerSubject::User(viewer),
207 ViewerSubject::Model(direct_model),
208 ],
209 "group-owned authorizations",
210 )?,
211 ),
212 )?
213 .access_id();
214 let member_view = check(
215 &stack,
216 RequestPrincipal::new(member, group_model),
217 access_id,
218 subsystem,
219 "owner-group member check",
220 )?;
221 require(
222 member_view.can_view() && member_view.can_manage() && member_view.target() == Some(&target),
223 "owner-group human/model permissions are wrong",
224 )?;
225 let model_view = check(
226 &stack,
227 RequestPrincipal::new(viewer, group_model),
228 access_id,
229 subsystem,
230 "owner-group model check",
231 )?;
232 require(
233 model_view.can_view() && !model_view.can_manage(),
234 "model owner-group membership granted management or lost viewing",
235 )?;
236
237 at(
238 "revoke owner-group model",
239 stack
240 .groups
241 .set_model_membership(owner, owner_group, group_model, false),
242 )?;
243 let revoked_model = check(
244 &stack,
245 RequestPrincipal::new(member, group_model),
246 access_id,
247 subsystem,
248 "model revocation check",
249 )?;
250 require(
251 !revoked_model.can_view() && revoked_model.can_manage(),
252 "model revocation did not remove view while preserving user management",
253 )?;
254 at(
255 "restore owner-group model",
256 stack
257 .groups
258 .set_model_membership(owner, owner_group, group_model, true),
259 )?;
260 at(
261 "revoke owner-group human",
262 stack.groups.set_user_role(owner, owner_group, member, None),
263 )?;
264 let revoked_user = check(
265 &stack,
266 RequestPrincipal::new(member, group_model),
267 access_id,
268 subsystem,
269 "user revocation check",
270 )?;
271 require(
272 !revoked_user.can_view() && !revoked_user.can_manage(),
273 "human revocation did not remove authorization",
274 )?;
275
276 let user_group = create_group(&stack, owner, "create user viewer group")?;
277 at(
278 "add user viewer group member",
279 stack
280 .groups
281 .set_user_role(owner, user_group, cross_user, Some(GroupRole::User)),
282 )?;
283 let model_group = create_group(&stack, owner, "create model viewer group")?;
284 at(
285 "add model viewer group member",
286 stack
287 .groups
288 .set_model_membership(owner, model_group, cross_model, true),
289 )?;
290 at(
291 "install cross-group authorizations",
292 stack.access.set_authorizations(
293 RequestPrincipal::new(owner, model(13)),
294 access_id,
295 auth(
296 vec![OwnerSubject::User(owner)],
297 vec![
298 ViewerSubject::Group(user_group),
299 ViewerSubject::Group(model_group),
300 ],
301 "cross-group authorizations",
302 )?,
303 ),
304 )?;
305 let cross = check(
306 &stack,
307 RequestPrincipal::new(cross_user, cross_model),
308 access_id,
309 subsystem,
310 "different viewer groups check",
311 )?;
312 require(
313 cross.can_view() && !cross.can_manage(),
314 "user and model could not qualify through different groups",
315 )?;
316
317 let discovery_access = at(
318 "create group discovery access",
319 stack.access.create(
320 Target::new(subsystem, vec![2, 0, 2, 0]),
321 auth(
322 vec![OwnerSubject::Group(owner_group)],
323 vec![
324 ViewerSubject::Group(user_group),
325 ViewerSubject::Group(model_group),
326 ],
327 "group discovery authorizations",
328 )?,
329 ),
330 )?
331 .access_id();
332 listed(
333 &stack,
334 RequestPrincipal::new(owner, model(1)),
335 owner_group,
336 subsystem,
337 &[access_id, discovery_access],
338 "owner group receives creation discovery",
339 )?;
340 listed(
341 &stack,
342 RequestPrincipal::new(cross_user, model(4)),
343 user_group,
344 subsystem,
345 &[access_id, discovery_access],
346 "viewer group receives creation discovery",
347 )?;
348 require(
349 at(
350 "model-only user personal discovery",
351 stack
352 .access
353 .list_user(RequestPrincipal::new(late_user, cross_model), subsystem),
354 )?
355 .is_empty(),
356 "model membership created personal discovery",
357 )?;
358 let model_only = stack.access.list_group(
359 RequestPrincipal::new(late_user, cross_model),
360 model_group,
361 subsystem,
362 );
363 require(
364 model_only == Err("access denied".to_owned()),
365 "model membership created a human group discovery path",
366 )?;
367 at(
368 "add late viewer group member",
369 stack
370 .groups
371 .set_user_role(owner, user_group, late_user, Some(GroupRole::User)),
372 )?;
373 listed(
374 &stack,
375 RequestPrincipal::new(late_user, model(5)),
376 user_group,
377 subsystem,
378 &[access_id, discovery_access],
379 "late member lists historical group discovery",
380 )?;
381 at(
382 "remove late viewer group member",
383 stack
384 .groups
385 .set_user_role(owner, user_group, late_user, None),
386 )?;
387 let removed = stack.access.list_group(
388 RequestPrincipal::new(late_user, model(5)),
389 user_group,
390 subsystem,
391 );
392 require(
393 removed == Err("access denied".to_owned()),
394 "removed user retained group listing",
395 )?;
396 listed(
397 &stack,
398 RequestPrincipal::new(cross_user, model(4)),
399 user_group,
400 subsystem,
401 &[access_id, discovery_access],
402 "current member retains group discovery",
403 )?;
404 require(
405 at(
406 "removed user personal discovery",
407 stack
408 .access
409 .list_user(RequestPrincipal::new(late_user, model(5)), subsystem),
410 )?
411 .is_empty(),
412 "group discovery was copied into removed user discovery",
413 )?;
414
415 let replacement_group = create_group(&stack, owner, "create replacement viewer group")?;
416 at(
417 "add replacement viewer group member",
418 stack
419 .groups
420 .set_user_role(owner, replacement_group, late_user, Some(GroupRole::User)),
421 )?;
422 at(
423 "add replacement discovery group",
424 stack.access.set_authorizations(
425 RequestPrincipal::new(owner, model(13)),
426 discovery_access,
427 auth(
428 vec![OwnerSubject::Group(owner_group)],
429 vec![
430 ViewerSubject::Group(user_group),
431 ViewerSubject::Group(model_group),
432 ViewerSubject::Group(replacement_group),
433 ],
434 "expanded group discovery authorizations",
435 )?,
436 ),
437 )?;
438 listed(
439 &stack,
440 RequestPrincipal::new(late_user, model(5)),
441 replacement_group,
442 subsystem,
443 &[discovery_access],
444 "added authorization group receives discovery",
445 )?;
446 at(
447 "remove replacement discovery group",
448 stack.access.set_authorizations(
449 RequestPrincipal::new(owner, model(13)),
450 discovery_access,
451 auth(
452 vec![OwnerSubject::Group(owner_group)],
453 vec![
454 ViewerSubject::Group(user_group),
455 ViewerSubject::Group(model_group),
456 ],
457 "reduced group discovery authorizations",
458 )?,
459 ),
460 )?;
461 listed(
462 &stack,
463 RequestPrincipal::new(late_user, model(5)),
464 replacement_group,
465 subsystem,
466 &[discovery_access],
467 "removed authorization group retains stale discovery",
468 )
469}
470
471#[cfg(test)]
472mod tests {
473 #[test]
474 fn groups_backed_access_conformance() {
475 super::verify().unwrap();
476 }
477}