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
96pub fn verify() -> Result<(), String> {
97    let root = tempfile::tempdir().map_err(|error| format!("create root: {error}"))?;
98    let stack = Stack::open(root.path())?;
99    let owner = user(1);
100    let member = user(2);
101    let viewer = user(3);
102    let cross_user = user(4);
103    let group_model = model(11);
104    let direct_model = model(12);
105    let cross_model = model(14);
106    let subsystem = SubsystemId::from_str("audio-fragment")?;
107
108    let sentinel_target = Target::new(subsystem, vec![0, 1, 0, 1]);
109    let sentinel_access = at(
110        "create sentinel access",
111        stack.access.create(
112            sentinel_target.clone(),
113            auth(
114                vec![OwnerSubject::User(owner)],
115                vec![
116                    ViewerSubject::Group(ALL_USERS),
117                    ViewerSubject::Group(ALL_MODELS),
118                ],
119                "sentinel authorizations",
120            )?,
121        ),
122    )?
123    .access_id();
124    let sentinel = check(
125        &stack,
126        RequestPrincipal::new(user(21), model(21)),
127        sentinel_access,
128        subsystem,
129        "sentinel viewer check",
130    )?;
131    require(
132        sentinel.can_view()
133            && !sentinel.can_manage()
134            && sentinel.target() == Some(&sentinel_target),
135        "user/model sentinels did not grant dimensioned view-only access",
136    )?;
137
138    let current_user = user(22);
139    let built_in_target = Target::new(subsystem, vec![0, 2, 0, 2]);
140    let built_in_access = at(
141        "create built-in-shape access",
142        stack.access.create(
143            built_in_target.clone(),
144            auth(
145                vec![OwnerSubject::User(current_user)],
146                vec![ViewerSubject::Group(ALL_MODELS)],
147                "built-in-shape authorizations",
148            )?,
149        ),
150    )?
151    .access_id();
152    let built_in = check(
153        &stack,
154        RequestPrincipal::new(current_user, model(22)),
155        built_in_access,
156        subsystem,
157        "built-in-shape check",
158    )?;
159    require(
160        built_in.can_view() && built_in.can_manage() && built_in.target() == Some(&built_in_target),
161        "built-in-profile authorization shape did not grant view and management",
162    )?;
163
164    let owner_group = create_group(&stack, owner, "create owner group")?;
165    at(
166        "add owner-group user",
167        stack
168            .groups
169            .set_user_role(owner, owner_group, member, Some(GroupRole::User)),
170    )?;
171    at(
172        "add owner-group model",
173        stack
174            .groups
175            .set_model_membership(owner, owner_group, group_model, true),
176    )?;
177    let target = Target::new(subsystem, vec![0, 255, 7, 0]);
178    let access_id = at(
179        "create group-owned access",
180        stack.access.create(
181            target.clone(),
182            auth(
183                vec![OwnerSubject::Group(owner_group)],
184                vec![
185                    ViewerSubject::User(viewer),
186                    ViewerSubject::Model(direct_model),
187                ],
188                "group-owned authorizations",
189            )?,
190        ),
191    )?
192    .access_id();
193    let member_view = check(
194        &stack,
195        RequestPrincipal::new(member, group_model),
196        access_id,
197        subsystem,
198        "owner-group member check",
199    )?;
200    require(
201        member_view.can_view() && member_view.can_manage() && member_view.target() == Some(&target),
202        "owner-group human/model permissions are wrong",
203    )?;
204    let model_view = check(
205        &stack,
206        RequestPrincipal::new(viewer, group_model),
207        access_id,
208        subsystem,
209        "owner-group model check",
210    )?;
211    require(
212        model_view.can_view() && !model_view.can_manage(),
213        "model owner-group membership granted management or lost viewing",
214    )?;
215
216    at(
217        "revoke owner-group model",
218        stack
219            .groups
220            .set_model_membership(owner, owner_group, group_model, false),
221    )?;
222    let revoked_model = check(
223        &stack,
224        RequestPrincipal::new(member, group_model),
225        access_id,
226        subsystem,
227        "model revocation check",
228    )?;
229    require(
230        !revoked_model.can_view() && revoked_model.can_manage(),
231        "model revocation did not remove view while preserving user management",
232    )?;
233    at(
234        "restore owner-group model",
235        stack
236            .groups
237            .set_model_membership(owner, owner_group, group_model, true),
238    )?;
239    at(
240        "revoke owner-group human",
241        stack.groups.set_user_role(owner, owner_group, member, None),
242    )?;
243    let revoked_user = check(
244        &stack,
245        RequestPrincipal::new(member, group_model),
246        access_id,
247        subsystem,
248        "user revocation check",
249    )?;
250    require(
251        !revoked_user.can_view() && !revoked_user.can_manage(),
252        "human revocation did not remove authorization",
253    )?;
254
255    let user_group = create_group(&stack, owner, "create user viewer group")?;
256    at(
257        "add user viewer group member",
258        stack
259            .groups
260            .set_user_role(owner, user_group, cross_user, Some(GroupRole::User)),
261    )?;
262    let model_group = create_group(&stack, owner, "create model viewer group")?;
263    at(
264        "add model viewer group member",
265        stack
266            .groups
267            .set_model_membership(owner, model_group, cross_model, true),
268    )?;
269    at(
270        "install cross-group authorizations",
271        stack.access.set_authorizations(
272            RequestPrincipal::new(owner, model(13)),
273            access_id,
274            auth(
275                vec![OwnerSubject::User(owner)],
276                vec![
277                    ViewerSubject::Group(user_group),
278                    ViewerSubject::Group(model_group),
279                ],
280                "cross-group authorizations",
281            )?,
282        ),
283    )?;
284    let cross = check(
285        &stack,
286        RequestPrincipal::new(cross_user, cross_model),
287        access_id,
288        subsystem,
289        "different viewer groups check",
290    )?;
291    require(
292        cross.can_view() && !cross.can_manage(),
293        "user and model could not qualify through different groups",
294    )
295}
296
297#[cfg(test)]
298mod tests {
299    #[test]
300    fn groups_backed_access_conformance() {
301        super::verify().unwrap();
302    }
303}