kcode-k1-ktool-set-launch-node 0.1.2

Model-facing launch-node Ktool operations for K1
Documentation
#![doc = include_str!("../Documentation.md")]

use std::sync::Arc;

use kcode_k1_access_launch_nodes::{
    AccessContext, AccessId, AccessPolicy, Authority, GroupId, K1AccessLaunchNodes, ProfileId,
    TargetId, TargetName, TxId, UserId,
};
use serde::Deserialize;

const INVALID_SET: &str = "invalid SetLaunchNode arguments";
const INVALID_GET: &str = "invalid GetLaunchNode arguments";

#[derive(Clone)]
pub struct SetLaunchNodeKtool {
    launch_nodes: Arc<K1AccessLaunchNodes>,
}

impl SetLaunchNodeKtool {
    pub fn new(launch_nodes: Arc<K1AccessLaunchNodes>) -> Self {
        Self { launch_nodes }
    }

    pub fn get_launch_node(&self) -> GetLaunchNodeKtool {
        GetLaunchNodeKtool {
            launch_nodes: Arc::clone(&self.launch_nodes),
        }
    }

    pub fn launch(
        &self,
        context: &AccessContext,
        profile_id: ProfileId,
        policy: AccessPolicy,
        arguments: &str,
    ) -> Result<String, String> {
        let parsed: SetArguments = serde_json::from_str(arguments).map_err(|_| invalid_set())?;
        let target_name = TargetName::new(parsed.target).map_err(|_| invalid_set())?;
        let node = parsed
            .node_id
            .parse::<TxId>()
            .map(AccessId::new)
            .map_err(|_| invalid_set())?;
        let target = TargetId::new(Authority::User(context.user()), target_name);
        self.launch_nodes
            .set(context, profile_id, policy, target, node)?;
        Ok("success".to_owned())
    }
}

#[derive(Clone)]
pub struct GetLaunchNodeKtool {
    launch_nodes: Arc<K1AccessLaunchNodes>,
}

impl GetLaunchNodeKtool {
    pub fn launch(&self, context: &AccessContext, arguments: &str) -> Result<String, String> {
        let parsed: GetArguments = serde_json::from_str(arguments).map_err(|_| invalid_get())?;
        let authority_id = parsed
            .authority_id
            .parse::<TxId>()
            .map_err(|_| invalid_get())?;
        let authority = match parsed.authority_kind.as_str() {
            "user" => Authority::User(UserId::from_tx_id(authority_id)),
            "group" => Authority::Group(GroupId::new(authority_id)),
            _ => return Err(invalid_get()),
        };
        let target_name = TargetName::new(parsed.target).map_err(|_| invalid_get())?;
        let node = self
            .launch_nodes
            .lookup(context, &TargetId::new(authority, target_name))?;
        Ok(format!(r#"{{"node_id":"{}"}}"#, node.txid()))
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SetArguments {
    target: String,
    node_id: String,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct GetArguments {
    authority_kind: String,
    authority_id: String,
    target: String,
}

fn invalid_set() -> String {
    INVALID_SET.to_owned()
}

fn invalid_get() -> String {
    INVALID_GET.to_owned()
}

#[cfg(test)]
mod tests {
    use super::*;
    use kcode_k1_access::{
        FilteredAuthorities, K1Access, ModelId, SubsystemId, Target, ViewerSubject,
    };
    use kcode_k1_groups::K1Groups;
    use kcode_k1_launch_nodes::LaunchNodes;
    use kcode_k1_peering::K1Peering;
    use kcode_k1_txn_ordering::K1TxnOrdering;

    fn tx(value: u8) -> TxId {
        TxId::from_bytes([value; 12])
    }

    #[test]
    fn strict_set_arguments_reject_every_malformed_shape() {
        let valid = r#"{"target":"Harness","node_id":"000000000000000000000001"}"#;
        assert!(serde_json::from_str::<SetArguments>(valid).is_ok());
        for arguments in [
            "",
            "{}",
            "[]",
            r#"{"target":"Harness"}"#,
            r#"{"node_id":"000000000000000000000001"}"#,
            r#"{"target":"Harness","node_id":"000000000000000000000001","extra":true}"#,
        ] {
            assert_eq!(
                serde_json::from_str::<SetArguments>(arguments)
                    .map(|_| ())
                    .map_err(|_| invalid_set()),
                Err(INVALID_SET.to_owned())
            );
        }
    }

    #[test]
    fn strict_get_arguments_reject_malformed_authority_selectors() {
        let valid = r#"{"authority_kind":"user","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#;
        assert!(serde_json::from_str::<GetArguments>(valid).is_ok());
        for arguments in [
            "",
            "{}",
            r#"{"authority_kind":"user","authority_id":"000000000000000000000001"}"#,
            r#"{"authority_kind":"user","authority_id":"000000000000000000000001","target":"KmapLaunchNode","extra":true}"#,
            r#"{"authority_kind":"user","authority_kind":"group","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#,
        ] {
            assert_eq!(
                serde_json::from_str::<GetArguments>(arguments)
                    .map(|_| ())
                    .map_err(|_| invalid_get()),
                Err(INVALID_GET.to_owned())
            );
        }
        for arguments in [
            r#"{"authority_kind":"friend","authority_id":"000000000000000000000001","target":"KmapLaunchNode"}"#,
            r#"{"authority_kind":"user","authority_id":"00000000000000000000000A","target":"KmapLaunchNode"}"#,
            r#"{"authority_kind":"group","authority_id":"1","target":"KmapLaunchNode"}"#,
            r#"{"authority_kind":"group","authority_id":"000000000000000000000001","target":" "}"#,
        ] {
            let root = tempfile::tempdir().unwrap();
            let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
            let peering =
                Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
            let groups = Arc::new(
                K1Groups::open(
                    &root.path().join("groups"),
                    ordering.clone(),
                    peering.clone(),
                )
                .unwrap(),
            );
            let access = Arc::new(
                K1Access::open(
                    &root.path().join("access"),
                    ordering.clone(),
                    peering.clone(),
                    groups.clone(),
                )
                .unwrap(),
            );
            let bindings = Arc::new(
                LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
            );
            let facade = Arc::new(K1AccessLaunchNodes::open(access, groups, bindings).unwrap());
            let context = AccessContext::new(
                UserId::from_tx_id(tx(1)),
                ModelId::from_bytes([2; 32]),
                FilteredAuthorities::new(Vec::new()).unwrap(),
            )
            .unwrap();
            assert_eq!(
                SetLaunchNodeKtool::new(facade)
                    .get_launch_node()
                    .launch(&context, arguments),
                Err(INVALID_GET.to_owned())
            );
        }
    }

    #[test]
    fn set_and_cross_user_get_share_the_facade_without_mutating_lookup() {
        let root = tempfile::tempdir().unwrap();
        let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
        let peering =
            Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
        let groups = Arc::new(
            K1Groups::open(
                &root.path().join("groups"),
                ordering.clone(),
                peering.clone(),
            )
            .unwrap(),
        );
        let access = Arc::new(
            K1Access::open(
                &root.path().join("access"),
                ordering.clone(),
                peering.clone(),
                groups.clone(),
            )
            .unwrap(),
        );
        let bindings = Arc::new(
            LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
        );
        let facade =
            Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings.clone()).unwrap());
        let setter = SetLaunchNodeKtool::new(facade.clone());
        let getter = setter.get_launch_node();

        let caller = UserId::from_tx_id(tx(1));
        let friend = UserId::from_tx_id(tx(8));
        let model = ModelId::from_bytes([2; 32]);
        let caller_context =
            AccessContext::new(caller, model, FilteredAuthorities::new(Vec::new()).unwrap())
                .unwrap();
        let friend_context =
            AccessContext::new(friend, model, FilteredAuthorities::new(Vec::new()).unwrap())
                .unwrap();
        let friend_profile = ProfileId::new(tx(3));
        let friend_policy = AccessPolicy::new(
            Authority::User(friend),
            Vec::new(),
            vec![ViewerSubject::User(caller), ViewerSubject::Model(model)],
        )
        .unwrap();
        let node = access
            .create(
                &friend_context,
                Target::new(SubsystemId::from_str("k1-kmap").unwrap(), vec![4; 12]),
                friend_profile,
                friend_policy.clone(),
            )
            .unwrap()
            .access_id();
        let target = TargetId::new(
            Authority::User(friend),
            TargetName::new("KmapLaunchNode".to_owned()).unwrap(),
        );
        facade
            .create(
                &friend_context,
                friend_profile,
                friend_policy,
                target.clone(),
                node,
            )
            .unwrap();
        let before = bindings.get(&target).unwrap();
        let arguments = format!(
            r#"{{"authority_kind":"user","authority_id":"{}","target":"KmapLaunchNode"}}"#,
            friend.as_tx_id()
        );
        assert_eq!(
            getter.launch(&caller_context, &arguments),
            Ok(format!(r#"{{"node_id":"{}"}}"#, node.txid()))
        );
        assert_eq!(bindings.get(&target).unwrap(), before);
        assert_eq!(facade.lookup(&caller_context, &target), Ok(node));
    }

    #[test]
    fn existing_set_create_and_update_use_the_context_user() {
        let root = tempfile::tempdir().unwrap();
        let ordering = Arc::new(K1TxnOrdering::open(&root.path().join("ordering")).unwrap());
        let peering =
            Arc::new(K1Peering::open(&root.path().join("peering"), ordering.clone()).unwrap());
        let groups = Arc::new(
            K1Groups::open(
                &root.path().join("groups"),
                ordering.clone(),
                peering.clone(),
            )
            .unwrap(),
        );
        let access = Arc::new(
            K1Access::open(
                &root.path().join("access"),
                ordering.clone(),
                peering.clone(),
                groups.clone(),
            )
            .unwrap(),
        );
        let bindings = Arc::new(
            LaunchNodes::open(&root.path().join("launch-nodes"), ordering, peering).unwrap(),
        );
        let facade = Arc::new(K1AccessLaunchNodes::open(access.clone(), groups, bindings).unwrap());
        let tool = SetLaunchNodeKtool::new(facade.clone());

        let user = UserId::from_tx_id(tx(1));
        let model = ModelId::from_bytes([2; 32]);
        let context =
            AccessContext::new(user, model, FilteredAuthorities::new(Vec::new()).unwrap()).unwrap();
        let profile = ProfileId::new(tx(3));
        let policy = AccessPolicy::new(
            Authority::User(user),
            Vec::new(),
            vec![ViewerSubject::Model(model)],
        )
        .unwrap();
        let subsystem = SubsystemId::from_str("k1-kmap").unwrap();
        let first = access
            .create(
                &context,
                Target::new(subsystem, vec![4; 12]),
                profile,
                policy.clone(),
            )
            .unwrap()
            .access_id();
        let second = access
            .create(
                &context,
                Target::new(subsystem, vec![5; 12]),
                profile,
                policy.clone(),
            )
            .unwrap()
            .access_id();
        let target = TargetId::new(
            Authority::User(user),
            TargetName::new("Harness".to_owned()).unwrap(),
        );
        let first_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, first.txid());
        assert_eq!(
            tool.launch(&context, profile, policy.clone(), &first_arguments),
            Ok("success".to_owned())
        );
        assert_eq!(facade.lookup(&context, &target), Ok(first));
        let second_arguments = format!(r#"{{"target":"Harness","node_id":"{}"}}"#, second.txid());
        assert_eq!(
            tool.launch(&context, profile, policy, &second_arguments),
            Ok("success".to_owned())
        );
        assert_eq!(facade.lookup(&context, &target), Ok(second));
    }

    #[test]
    fn complete_package_stays_below_the_managed_limit() {
        let files = [
            include_str!("../Cargo.toml"),
            include_str!("../Documentation.md"),
            include_str!("lib.rs"),
        ];
        let count = files
            .iter()
            .flat_map(|file| file.lines())
            .filter(|line| !line.trim().is_empty())
            .count();
        assert!(count < 500, "complete package has {count} nonblank lines");
    }
}