Skip to main content

kcode_k1_access_groups_testkit/
lib.rs

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}