1#![doc = include_str!("../Documentation.md")]
2
3use std::sync::Arc;
4
5use kcode_k1_access_launch_nodes::{
6 AccessContext, AccessId, AccessPolicy, Authority, GroupId, K1AccessLaunchNodes, ProfileId,
7 TargetId, TargetName, TxId, UserId,
8};
9use serde::Deserialize;
10
11const INVALID_SET: &str = "invalid SetLaunchNode arguments";
12const INVALID_GET: &str = "invalid GetLaunchNode arguments";
13
14#[derive(Clone)]
15pub struct SetLaunchNodeKtool {
16 launch_nodes: Arc<K1AccessLaunchNodes>,
17}
18
19impl SetLaunchNodeKtool {
20 pub fn new(launch_nodes: Arc<K1AccessLaunchNodes>) -> Self {
21 Self { launch_nodes }
22 }
23
24 pub fn get_launch_node(&self) -> GetLaunchNodeKtool {
25 GetLaunchNodeKtool {
26 launch_nodes: Arc::clone(&self.launch_nodes),
27 }
28 }
29
30 pub fn launch(
31 &self,
32 context: &AccessContext,
33 profile_id: ProfileId,
34 policy: AccessPolicy,
35 arguments: &str,
36 ) -> Result<String, String> {
37 let parsed: SetArguments = serde_json::from_str(arguments).map_err(|_| invalid_set())?;
38 let target_name = TargetName::new(parsed.target).map_err(|_| invalid_set())?;
39 let node = parsed
40 .node_id
41 .parse::<TxId>()
42 .map(AccessId::new)
43 .map_err(|_| invalid_set())?;
44 let target = TargetId::new(Authority::User(context.user()), target_name);
45 self.launch_nodes
46 .set(context, profile_id, policy, target, node)?;
47 Ok("success".to_owned())
48 }
49}
50
51#[derive(Clone)]
52pub struct GetLaunchNodeKtool {
53 launch_nodes: Arc<K1AccessLaunchNodes>,
54}
55
56impl GetLaunchNodeKtool {
57 pub fn launch(&self, context: &AccessContext, arguments: &str) -> Result<String, String> {
58 let parsed: GetArguments = serde_json::from_str(arguments).map_err(|_| invalid_get())?;
59 let authority_id = parsed
60 .authority_id
61 .parse::<TxId>()
62 .map_err(|_| invalid_get())?;
63 let authority = match parsed.authority_kind.as_str() {
64 "user" => Authority::User(UserId::from_tx_id(authority_id)),
65 "group" => Authority::Group(GroupId::new(authority_id)),
66 _ => return Err(invalid_get()),
67 };
68 let target_name = TargetName::new(parsed.target).map_err(|_| invalid_get())?;
69 let node = self
70 .launch_nodes
71 .lookup(context, &TargetId::new(authority, target_name))?;
72 Ok(format!(r#"{{"node_id":"{}"}}"#, node.txid()))
73 }
74}
75
76#[derive(Deserialize)]
77#[serde(deny_unknown_fields)]
78struct SetArguments {
79 target: String,
80 node_id: String,
81}
82
83#[derive(Deserialize)]
84#[serde(deny_unknown_fields)]
85struct GetArguments {
86 authority_kind: String,
87 authority_id: String,
88 target: String,
89}
90
91fn invalid_set() -> String {
92 INVALID_SET.to_owned()
93}
94
95fn invalid_get() -> String {
96 INVALID_GET.to_owned()
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use kcode_k1_access::{
103 FilteredAuthorities, K1Access, ModelId, SubsystemId, Target, ViewerSubject,
104 };
105 use kcode_k1_groups::K1Groups;
106 use kcode_k1_launch_nodes::LaunchNodes;
107 use kcode_k1_peering::K1Peering;
108 use kcode_k1_txn_ordering::K1TxnOrdering;
109
110 fn tx(value: u8) -> TxId {
111 TxId::from_bytes([value; 12])
112 }
113
114 #[test]
115 fn strict_set_arguments_reject_every_malformed_shape() {
116 let valid = r#"{"target":"Harness","node_id":"000000000000000000000001"}"#;
117 assert!(serde_json::from_str::<SetArguments>(valid).is_ok());
118 for arguments in [
119 "",
120 "{}",
121 "[]",
122 r#"{"target":"Harness"}"#,
123 r#"{"node_id":"000000000000000000000001"}"#,
124 r#"{"target":"Harness","node_id":"000000000000000000000001","extra":true}"#,
125 ] {
126 assert_eq!(
127 serde_json::from_str::<SetArguments>(arguments)
128 .map(|_| ())
129 .map_err(|_| invalid_set()),
130 Err(INVALID_SET.to_owned())
131 );
132 }
133 }
134
135 #[test]
136 fn strict_get_arguments_reject_malformed_authority_selectors() {
137 let valid = r#"{"authority_kind":"user","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#;
138 assert!(serde_json::from_str::<GetArguments>(valid).is_ok());
139 for arguments in [
140 "",
141 "{}",
142 r#"{"authority_kind":"user","authority_id":"000000000000000000000001"}"#,
143 r#"{"authority_kind":"user","authority_id":"000000000000000000000001","target":"KmapLaunchNode","extra":true}"#,
144 r#"{"authority_kind":"user","authority_kind":"group","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#,
145 ] {
146 assert_eq!(
147 serde_json::from_str::<GetArguments>(arguments)
148 .map(|_| ())
149 .map_err(|_| invalid_get()),
150 Err(INVALID_GET.to_owned())
151 );
152 }
153 for arguments in [
154 r#"{"authority_kind":"friend","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#,
155 r#"{"authority_kind":"user","authority_id":"00000000000000000000000A","target":"KmapLaunchNode"}"#,
156 r#"{"authority_kind":"group","authority_id":"1","target":"KmapLaunchNode"}"#,
157 r#"{"authority_kind":"group","authority_id":"000000000000000000000001","target":" "}"#,
158 ] {
159 let root = tempfile::tempdir().unwrap();
160 let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
161 let peering =
162 Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
163 let groups = Arc::new(
164 K1Groups::open(
165 &root.path().join("groups"),
166 ordering.clone(),
167 peering.clone(),
168 )
169 .unwrap(),
170 );
171 let access = Arc::new(
172 K1Access::open(
173 &root.path().join("access"),
174 ordering.clone(),
175 peering.clone(),
176 groups.clone(),
177 )
178 .unwrap(),
179 );
180 let bindings = Arc::new(
181 LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
182 );
183 let facade = Arc::new(K1AccessLaunchNodes::open(access, groups, bindings).unwrap());
184 let context = AccessContext::new(
185 UserId::from_tx_id(tx(1)),
186 ModelId::from_bytes([2; 32]),
187 FilteredAuthorities::new(Vec::new()).unwrap(),
188 )
189 .unwrap();
190 assert_eq!(
191 SetLaunchNodeKtool::new(facade)
192 .get_launch_node()
193 .launch(&context, arguments),
194 Err(INVALID_GET.to_owned())
195 );
196 }
197 }
198
199 #[test]
200 fn set_and_cross_user_get_share_the_facade_without_mutating_lookup() {
201 let root = tempfile::tempdir().unwrap();
202 let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
203 let peering =
204 Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
205 let groups = Arc::new(
206 K1Groups::open(
207 &root.path().join("groups"),
208 ordering.clone(),
209 peering.clone(),
210 )
211 .unwrap(),
212 );
213 let access = Arc::new(
214 K1Access::open(
215 &root.path().join("access"),
216 ordering.clone(),
217 peering.clone(),
218 groups.clone(),
219 )
220 .unwrap(),
221 );
222 let bindings = Arc::new(
223 LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
224 );
225 let facade =
226 Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings.clone()).unwrap());
227 let setter = SetLaunchNodeKtool::new(facade.clone());
228 let getter = setter.get_launch_node();
229
230 let caller = UserId::from_tx_id(tx(1));
231 let friend = UserId::from_tx_id(tx(8));
232 let model = ModelId::from_bytes([2; 32]);
233 let caller_context =
234 AccessContext::new(caller, model, FilteredAuthorities::new(Vec::new()).unwrap())
235 .unwrap();
236 let friend_context =
237 AccessContext::new(friend, model, FilteredAuthorities::new(Vec::new()).unwrap())
238 .unwrap();
239 let friend_profile = ProfileId::new(tx(3));
240 let friend_policy = AccessPolicy::new(
241 Authority::User(friend),
242 Vec::new(),
243 vec![ViewerSubject::User(caller), ViewerSubject::Model(model)],
244 )
245 .unwrap();
246 let node = access
247 .create(
248 &friend_context,
249 Target::new(SubsystemId::from_str("k1-kmap").unwrap(), vec![4; 12]),
250 friend_profile,
251 friend_policy.clone(),
252 )
253 .unwrap()
254 .access_id();
255 let target = TargetId::new(
256 Authority::User(friend),
257 TargetName::new("KmapLaunchNode".to_owned()).unwrap(),
258 );
259 facade
260 .create(
261 &friend_context,
262 friend_profile,
263 friend_policy,
264 target.clone(),
265 node,
266 )
267 .unwrap();
268 let before = bindings.get(&target).unwrap();
269 let arguments = format!(
270 r#"{{"authority_kind":"user","authority_id":"{}","target":"KmapLaunchNode"}}"#,
271 friend.as_tx_id()
272 );
273 assert_eq!(
274 getter.launch(&caller_context, &arguments),
275 Ok(format!(r#"{{"node_id":"{}"}}"#, node.txid()))
276 );
277 assert_eq!(bindings.get(&target).unwrap(), before);
278 assert_eq!(facade.lookup(&caller_context, &target), Ok(node));
279 }
280
281 #[test]
282 fn existing_set_create_and_update_use_the_context_user() {
283 let root = tempfile::tempdir().unwrap();
284 let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
285 let peering =
286 Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
287 let groups = Arc::new(
288 K1Groups::open(
289 &root.path().join("groups"),
290 ordering.clone(),
291 peering.clone(),
292 )
293 .unwrap(),
294 );
295 let access = Arc::new(
296 K1Access::open(
297 &root.path().join("access"),
298 ordering.clone(),
299 peering.clone(),
300 groups.clone(),
301 )
302 .unwrap(),
303 );
304 let bindings = Arc::new(
305 LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
306 );
307 let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
308 let tool = SetLaunchNodeKtool::new(facade.clone());
309
310 let user = UserId::from_tx_id(tx(1));
311 let model = ModelId::from_bytes([2; 32]);
312 let context =
313 AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
314 let profile = ProfileId::new(tx(3));
315 let policy = AccessPolicy::new(
316 Authority::User(user),
317 Vec::new(),
318 vec![ViewerSubject::Model(model)],
319 )
320 .unwrap();
321 let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
322 let first = access
323 .create(
324 &context,
325 Target::new(subsystem, vec![4; 12]),
326 profile,
327 policy.clone(),
328 )
329 .unwrap()
330 .access_id();
331 let second = access
332 .create(
333 &context,
334 Target::new(subsystem, vec![5; 12]),
335 profile,
336 policy.clone(),
337 )
338 .unwrap()
339 .access_id();
340 let target = TargetId::new(
341 Authority::User(user),
342 TargetName::new("Harness".to_owned()).unwrap(),
343 );
344 let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
345 assert_eq!(
346 tool.launch(&context, profile, policy.clone(), &first_arguments),
347 Ok("success".to_owned())
348 );
349 assert_eq!(facade.lookup(&context, &target), Ok(first));
350 let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
351 assert_eq!(
352 tool.launch(&context, profile, policy, &second_arguments),
353 Ok("success".to_owned())
354 );
355 assert_eq!(facade.lookup(&context, &target), Ok(second));
356 }
357
358 #[test]
359 fn complete_package_stays_below_the_managed_limit() {
360 let files = [
361 include_str!("../Cargo.toml"),
362 include_str!("../Documentation.md"),
363 include_str!("lib.rs"),
364 ];
365 let count = files
366 .iter()
367 .flat_map(|file| file.lines())
368 .filter(|line| !line.trim().is_empty())
369 .count();
370 assert!(count < 500, "complete package has {count} nonblank lines");
371 }
372}