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}