myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

services_hardening_runtime_context.rs (10895B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::error::Error;
      4 use std::path::{Path, PathBuf};
      5 
      6 use myc::{
      7     MycBootstrapProfileV1, MycRuntimeContextErrorKind, RadrootsHostEnvironment,
      8     RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, RuntimeContextSource,
      9     parse_myc_cli_v1_from, resolve_myc_runtime_context,
     10 };
     11 
     12 const MANIFEST: &str = include_str!("../Cargo.toml");
     13 const LIB_SOURCE: &str = include_str!("../src/lib.rs");
     14 const CONTEXT_SOURCE: &str = include_str!("../src/runtime_context.rs");
     15 
     16 fn resolve(
     17     resolver: &RadrootsPathResolver,
     18     profile: &str,
     19     instance: &str,
     20     repo_local_root: Option<&str>,
     21     config_path: Option<&str>,
     22 ) -> myc::MycRuntimeContext {
     23     let mut arguments = vec!["myc", "--profile", profile, "--instance", instance];
     24     if let Some(root) = repo_local_root {
     25         arguments.extend(["--repo-local-root", root]);
     26     }
     27     if let Some(path) = config_path {
     28         arguments.extend(["--config", path]);
     29     }
     30     arguments.push("run");
     31     let invocation = parse_myc_cli_v1_from(arguments).expect("validated CLI selection");
     32     resolve_myc_runtime_context(resolver, &invocation).expect("Myc runtime context")
     33 }
     34 
     35 fn assert_exact_roots(context: &myc::MycRuntimeContext, expected: [&str; 6]) {
     36     let paths = context.context().paths();
     37     assert_eq!(paths.config(), Path::new(expected[0]));
     38     assert_eq!(paths.state(), Path::new(expected[1]));
     39     assert_eq!(paths.cache(), Path::new(expected[2]));
     40     assert_eq!(paths.logs(), Path::new(expected[3]));
     41     assert_eq!(paths.run(), Path::new(expected[4]));
     42     assert_eq!(paths.secrets(), Path::new(expected[5]));
     43 }
     44 
     45 #[test]
     46 fn repo_local_context_binds_typed_identity_sources_and_exact_artifacts() {
     47     let resolver =
     48         RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
     49     let primary = resolve(
     50         &resolver,
     51         "repo-local",
     52         "primary",
     53         Some("/repo/.local/radroots"),
     54         None,
     55     );
     56     let secondary = resolve(
     57         &resolver,
     58         "repo-local",
     59         "secondary",
     60         Some("/repo/.local/radroots"),
     61         None,
     62     );
     63 
     64     assert_eq!(primary.context().service().as_str(), "myc");
     65     assert_eq!(primary.context().instance().as_str(), "primary");
     66     assert_eq!(primary.profile(), MycBootstrapProfileV1::RepoLocal);
     67     assert_eq!(primary.context().profile(), RadrootsPathProfile::RepoLocal);
     68     assert_eq!(
     69         primary.context().sources().service(),
     70         RuntimeContextSource::SafeDefault
     71     );
     72     assert_eq!(
     73         primary.context().sources().instance(),
     74         RuntimeContextSource::BootstrapCli
     75     );
     76     assert_eq!(
     77         primary.context().sources().profile(),
     78         RuntimeContextSource::BootstrapCli
     79     );
     80     assert_eq!(
     81         primary.context().sources().repo_local_root(),
     82         Some(RuntimeContextSource::BootstrapCli)
     83     );
     84     assert_eq!(
     85         primary.context().sources().paths(),
     86         RuntimeContextSource::DerivedPath
     87     );
     88     assert_exact_roots(
     89         &primary,
     90         [
     91             "/repo/.local/radroots/config/services/myc/primary",
     92             "/repo/.local/radroots/data/services/myc/primary",
     93             "/repo/.local/radroots/cache/services/myc/primary",
     94             "/repo/.local/radroots/logs/services/myc/primary",
     95             "/repo/.local/radroots/run/services/myc/primary",
     96             "/repo/.local/radroots/secrets/services/myc/primary",
     97         ],
     98     );
     99     assert_eq!(
    100         primary.artifacts().config(),
    101         Path::new("/repo/.local/radroots/config/services/myc/primary/config.toml")
    102     );
    103     assert_eq!(
    104         primary.artifacts().state_database(),
    105         Path::new("/repo/.local/radroots/data/services/myc/primary/state.sqlite")
    106     );
    107     assert_eq!(
    108         primary.artifacts().state_lock(),
    109         Path::new("/repo/.local/radroots/data/services/myc/primary/state.lock")
    110     );
    111     assert_eq!(
    112         primary.artifacts().admin_socket(),
    113         Path::new("/repo/.local/radroots/run/services/myc/primary/admin.sock")
    114     );
    115     assert_eq!(primary.selected_config_path(), primary.artifacts().config());
    116     assert_ne!(primary.context().paths(), secondary.context().paths());
    117     assert_eq!(secondary.context().instance().as_str(), "secondary");
    118 }
    119 
    120 #[test]
    121 fn service_host_and_interactive_profiles_use_the_shared_exact_roots() {
    122     let service_host = resolve(
    123         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
    124         "service-host",
    125         "primary",
    126         None,
    127         None,
    128     );
    129     assert_exact_roots(
    130         &service_host,
    131         [
    132             "/etc/radroots/services/myc/primary",
    133             "/var/lib/radroots/services/myc/primary",
    134             "/var/cache/radroots/services/myc/primary",
    135             "/var/log/radroots/services/myc/primary",
    136             "/run/radroots/services/myc/primary",
    137             "/etc/radroots/secrets/services/myc/primary",
    138         ],
    139     );
    140     assert_eq!(
    141         service_host.artifacts().admin_socket(),
    142         Path::new("/run/radroots/services/myc/primary/admin.sock")
    143     );
    144 
    145     let linux_interactive = resolve(
    146         &RadrootsPathResolver::new(
    147             RadrootsPlatform::Linux,
    148             RadrootsHostEnvironment {
    149                 home_dir: Some(PathBuf::from("/home/operator")),
    150                 xdg_config_home: Some(PathBuf::from("/xdg/config")),
    151                 xdg_data_home: Some(PathBuf::from("/xdg/data")),
    152                 xdg_state_home: Some(PathBuf::from("/xdg/state")),
    153                 xdg_cache_home: Some(PathBuf::from("/xdg/cache")),
    154                 xdg_runtime_dir: Some(PathBuf::from("/xdg/run")),
    155                 ..RadrootsHostEnvironment::default()
    156             },
    157         ),
    158         "interactive",
    159         "primary",
    160         None,
    161         None,
    162     );
    163     assert_exact_roots(
    164         &linux_interactive,
    165         [
    166             "/xdg/config/radroots/services/myc/primary",
    167             "/xdg/data/radroots/services/myc/primary",
    168             "/xdg/cache/radroots/services/myc/primary",
    169             "/xdg/state/radroots/logs/services/myc/primary",
    170             "/xdg/run/radroots/services/myc/primary",
    171             "/xdg/config/radroots/secrets/services/myc/primary",
    172         ],
    173     );
    174 }
    175 
    176 #[test]
    177 fn interactive_macos_and_windows_roots_remain_exact_and_injected() {
    178     let macos = resolve(
    179         &RadrootsPathResolver::new(
    180             RadrootsPlatform::Macos,
    181             RadrootsHostEnvironment {
    182                 home_dir: Some(PathBuf::from("/Users/operator")),
    183                 ..RadrootsHostEnvironment::default()
    184             },
    185         ),
    186         "interactive",
    187         "primary",
    188         None,
    189         None,
    190     );
    191     assert_exact_roots(
    192         &macos,
    193         [
    194             "/Users/operator/Library/Application Support/Radroots/config/services/myc/primary",
    195             "/Users/operator/Library/Application Support/Radroots/data/services/myc/primary",
    196             "/Users/operator/Library/Caches/Radroots/services/myc/primary",
    197             "/Users/operator/Library/Logs/Radroots/services/myc/primary",
    198             "/Users/operator/Library/Application Support/Radroots/run/services/myc/primary",
    199             "/Users/operator/Library/Application Support/Radroots/secrets/services/myc/primary",
    200         ],
    201     );
    202 
    203     let windows = resolve(
    204         &RadrootsPathResolver::new(
    205             RadrootsPlatform::Windows,
    206             RadrootsHostEnvironment {
    207                 appdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Roaming")),
    208                 localappdata_dir: Some(PathBuf::from(r"C:\Users\operator\AppData\Local")),
    209                 ..RadrootsHostEnvironment::default()
    210             },
    211         ),
    212         "interactive",
    213         "primary",
    214         None,
    215         None,
    216     );
    217     assert_exact_roots(
    218         &windows,
    219         [
    220             r"C:\Users\operator\AppData\Roaming/Radroots/config/services/myc/primary",
    221             r"C:\Users\operator\AppData\Local/Radroots/data/services/myc/primary",
    222             r"C:\Users\operator\AppData\Local/Radroots/cache/services/myc/primary",
    223             r"C:\Users\operator\AppData\Local/Radroots/logs/services/myc/primary",
    224             r"C:\Users\operator\AppData\Local/Radroots/run/services/myc/primary",
    225             r"C:\Users\operator\AppData\Roaming/Radroots/secrets/services/myc/primary",
    226         ],
    227     );
    228 }
    229 
    230 #[test]
    231 fn explicit_config_path_overrides_only_the_common_config_artifact_selection() {
    232     let resolver =
    233         RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default());
    234     let context = resolve(
    235         &resolver,
    236         "repo-local",
    237         "primary",
    238         Some("/repo/.local/radroots"),
    239         Some("/operator/config/myc.toml"),
    240     );
    241     assert_eq!(
    242         context.selected_config_path(),
    243         Path::new("/operator/config/myc.toml")
    244     );
    245     assert_eq!(
    246         context.artifacts().config(),
    247         Path::new("/repo/.local/radroots/config/services/myc/primary/config.toml")
    248     );
    249 }
    250 
    251 #[test]
    252 fn unsupported_profile_platform_and_diagnostics_fail_safely() {
    253     let invocation = parse_myc_cli_v1_from([
    254         "myc",
    255         "--profile",
    256         "service-host",
    257         "--instance",
    258         "secret-instance",
    259         "--config",
    260         "/private/secret-config.toml",
    261         "run",
    262     ])
    263     .expect("valid CLI");
    264     let error = resolve_myc_runtime_context(
    265         &RadrootsPathResolver::new(RadrootsPlatform::Macos, RadrootsHostEnvironment::default()),
    266         &invocation,
    267     )
    268     .expect_err("unsupported service-host platform");
    269     assert_eq!(error.kind(), MycRuntimeContextErrorKind::PathSelection);
    270     assert!(Error::source(&error).is_none());
    271     let rendered = format!("{error} {error:?}");
    272     assert!(!rendered.contains("secret-instance"));
    273     assert!(!rendered.contains("secret-config"));
    274 
    275     let context = resolve(
    276         &RadrootsPathResolver::new(RadrootsPlatform::Linux, RadrootsHostEnvironment::default()),
    277         "repo-local",
    278         "secret-instance",
    279         Some("/private/secret-root"),
    280         Some("/private/secret-config.toml"),
    281     );
    282     let debug = format!("{context:?}");
    283     assert!(!debug.contains("secret-instance"));
    284     assert!(!debug.contains("secret-root"));
    285     assert!(!debug.contains("secret-config"));
    286 }
    287 
    288 #[test]
    289 fn shared_runtime_paths_are_the_only_path_policy_and_identity_authority() {
    290     assert!(MANIFEST.contains(
    291         "radroots_runtime_paths = { git = \"https://github.com/radrootslabs/lib\", rev = \"055096853fca95e15d0f813d33a14aca13be3881\", version = \"=0.1.0-alpha\" }"
    292     ));
    293     assert!(LIB_SOURCE.contains("mod runtime_context;"));
    294     assert!(!LIB_SOURCE.contains("pub mod runtime_context;"));
    295     assert!(CONTEXT_SOURCE.contains("RuntimeContext::resolve("));
    296     assert!(CONTEXT_SOURCE.contains("default_service_instance_artifacts("));
    297     let root = Path::new(env!("CARGO_MANIFEST_DIR"));
    298     assert!(!root.join("src/paths.rs").exists());
    299     assert!(!root.join("src/config.rs").exists());
    300 }