Skip to main content

kcode_k1_ktool_set_launch_node/
lib.rs

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}