frame-core 0.3.0

Component model, lifecycle, process isolation — hosts components as supervised BEAM process trees
Documentation
//! End-to-end acceptance for F-1b's deny-by-default capability floor.

#![allow(clippy::expect_used, clippy::unwrap_used)]

use std::collections::HashMap;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};

use beamr::module::ModuleRegistry;
use beamr::scheduler::{Scheduler, SchedulerConfig, SchedulerServices};
use frame_core::capability::{
    Capability, CapabilityDenied, CapabilityMutationError, CapabilityRequest, CapabilityScope,
    CheckVerdict, Grant, GrantProvenance, NetworkHost, PathRoot, RevokeOutcome,
};
use frame_core::component::{ComponentId, ComponentMeta, SupervisionPolicy};
use frame_core::error::RegistryError;
use frame_core::event::{EventTryReceiveError, LifecycleEventKind, LifecycleState};
use frame_core::registry::ComponentRegistry;
use frame_core::supervision::LifecycleConfig;

const FFI: &[u8] = include_bytes!("fixtures/spike_worker_ffi.beam");
const TIMEOUT: Duration = Duration::from_secs(3);

fn id(name: &str) -> ComponentId {
    ComponentId::derive("frame.tests.capability", name)
}

fn request(capability: Capability) -> CapabilityRequest {
    CapabilityRequest::new(capability)
}

fn network(host: &str) -> CapabilityRequest {
    request(Capability::Network {
        host: NetworkHost(host.to_owned()),
    })
}

fn provenance(actor: &str, second: u64) -> GrantProvenance {
    GrantProvenance {
        granted_by: actor.to_owned(),
        granted_at: SystemTime::UNIX_EPOCH + Duration::from_secs(second),
    }
}

fn scheduler() -> Arc<Scheduler> {
    Arc::new(
        frame_core::composition::compose_scheduler(
            SchedulerConfig {
                thread_count: Some(2),
                ..SchedulerConfig::default()
            },
            SchedulerServices::minimal(),
            Arc::new(ModuleRegistry::new()),
        )
        .expect("published beamr scheduler starts"),
    )
}

fn registry(scheduler: &Arc<Scheduler>) -> Arc<ComponentRegistry> {
    Arc::new(ComponentRegistry::new(
        Arc::clone(scheduler),
        LifecycleConfig {
            operation_timeout: TIMEOUT,
            max_fragment_bytes: None,
        },
    ))
}

fn meta(name: &str, needs: Vec<CapabilityRequest>) -> ComponentMeta {
    ComponentMeta {
        id: id(name),
        name: name.to_owned(),
        version: "1.0.0".to_owned(),
        requires: Vec::new(),
        provides: Vec::new(),
        needs,
        actions: Vec::new(),
        fragments: Vec::new(),
        children: Vec::new(),
        supervision: Some(SupervisionPolicy {
            max_restarts: 1,
            window: Duration::from_secs(30),
        }),
    }
}

fn denied(verdict: CheckVerdict) -> Option<CapabilityDenied> {
    match verdict {
        CheckVerdict::Allowed => None,
        CheckVerdict::Denied(denial) => Some(denial),
    }
}

#[test]
fn registration_rejects_malformed_and_duplicate_needs_without_partial_state() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let malformed = request(Capability::FsRead {
        root: PathRoot(PathBuf::from("relative/path")),
    });
    assert!(matches!(
        registry.register(meta("malformed", vec![malformed.clone()]), FFI.to_vec()),
        Err(RegistryError::InvalidCapabilityScope { request, .. }) if request == malformed
    ));
    assert_eq!(registry.len().unwrap(), 0);

    let pattern = network("*.example.com");
    assert!(matches!(
        registry.register(meta("pattern", vec![pattern.clone()]), FFI.to_vec()),
        Err(RegistryError::InvalidCapabilityScope { request, .. }) if request == pattern
    ));
    assert_eq!(registry.len().unwrap(), 0);

    let duplicate = network("api.example.com");
    assert!(matches!(
        registry.register(
            meta("duplicate", vec![duplicate.clone(), duplicate.clone()]),
            FFI.to_vec()
        ),
        Err(RegistryError::DuplicateCapabilityNeed { request, .. }) if request == duplicate
    ));
    assert_eq!(registry.len().unwrap(), 0);
    scheduler.shutdown();
}

#[test]
fn declared_and_undeclared_absence_deny_typed_and_event_once_each() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("absence");
    let declared = network("api.example.com");
    registry
        .register(meta("absence", vec![declared.clone()]), FFI.to_vec())
        .unwrap();
    assert!(
        registry
            .host_capabilities()
            .grants_for(component)
            .unwrap()
            .is_empty()
    );
    let events = registry.subscribe(NonZeroUsize::new(4).unwrap()).unwrap();
    let checker = registry.capability_checker(component).unwrap();

    let declared_denial = denied(checker.check(&declared.capability).unwrap()).unwrap();
    assert_eq!(declared_denial.kind, declared.capability.kind());
    assert_eq!(declared_denial.scope, declared.capability.scope());
    assert!(declared_denial.declared);

    let undeclared = network("other.example.com");
    let undeclared_denial = denied(checker.check(&undeclared.capability).unwrap()).unwrap();
    assert_eq!(undeclared_denial.kind, undeclared.capability.kind());
    assert_eq!(undeclared_denial.scope, undeclared.capability.scope());
    assert!(!undeclared_denial.declared);

    for expected in [declared_denial, undeclared_denial] {
        let event = events.recv_timeout(TIMEOUT).unwrap();
        assert_eq!(event.kind, LifecycleEventKind::CapabilityDenied(expected));
    }
    assert_eq!(events.try_recv(), Err(EventTryReceiveError::Empty));
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn retained_checker_observes_revoke_on_the_very_next_consuming_act() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("retained");
    let need = network("api.example.com");
    registry
        .register(meta("retained", vec![need.clone()]), FFI.to_vec())
        .unwrap();
    let checker = registry.capability_checker(component).unwrap();
    registry
        .host_capabilities()
        .grant(component, need.clone(), provenance("composition", 1))
        .unwrap();
    assert_eq!(
        checker.check(&need.capability).unwrap(),
        CheckVerdict::Allowed
    );

    let consuming_act = || checker.check(&need.capability);
    assert!(matches!(
        registry
            .host_capabilities()
            .revoke(component, &need)
            .unwrap(),
        RevokeOutcome::Revoked(_)
    ));
    let denial = denied(consuming_act().unwrap()).unwrap();
    assert!(denial.declared);
    assert_eq!(
        registry
            .host_capabilities()
            .revoke(component, &need)
            .unwrap(),
        RevokeOutcome::NotGranted
    );
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn grant_for_undeclared_need_is_a_typed_refusal() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("undeclared-grant");
    registry
        .register(meta("undeclared-grant", Vec::new()), FFI.to_vec())
        .unwrap();
    let undeclared = network("api.example.com");
    assert!(matches!(
        registry.host_capabilities().grant(
            component,
            undeclared.clone(),
            provenance("composition", 1)
        ),
        Err(CapabilityMutationError::UndeclaredNeed {
            component_id,
            request
        }) if component_id == component && request == undeclared
    ));
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn removal_clears_authority_before_same_id_reincarnation() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("reincarnation");
    let need = network("api.example.com");
    registry
        .register(meta("reincarnation", vec![need.clone()]), FFI.to_vec())
        .unwrap();
    registry
        .host_capabilities()
        .grant(component, need.clone(), provenance("composition", 1))
        .unwrap();
    let old_checker = registry.capability_checker(component).unwrap();
    registry.remove(component).unwrap();
    assert!(matches!(
        old_checker.check(&need.capability).unwrap(),
        CheckVerdict::Denied(_)
    ));

    registry
        .register(meta("reincarnation", vec![need.clone()]), FFI.to_vec())
        .unwrap();
    assert!(
        registry
            .host_capabilities()
            .grants_for(component)
            .unwrap()
            .is_empty()
    );
    let fresh = registry.capability_checker(component).unwrap();
    assert!(matches!(
        fresh.check(&need.capability).unwrap(),
        CheckVerdict::Denied(CapabilityDenied { declared: true, .. })
    ));
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn full_table_reconstructs_mutations_with_exact_provenance() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("inspection");
    let first = network("one.example.com");
    let second = network("two.example.com");
    registry
        .register(
            meta("inspection", vec![first.clone(), second.clone()]),
            FFI.to_vec(),
        )
        .unwrap();
    let first_grant = registry
        .host_capabilities()
        .grant(component, first.clone(), provenance("operator-a", 10))
        .unwrap();
    let second_grant = registry
        .host_capabilities()
        .grant(component, second.clone(), provenance("operator-b", 20))
        .unwrap();
    let revoked = registry
        .host_capabilities()
        .revoke(component, &first)
        .unwrap();

    let mut reconstructed = HashMap::<CapabilityRequest, Grant>::new();
    reconstructed.insert(first, first_grant);
    reconstructed.insert(second.clone(), second_grant.clone());
    let row = match revoked {
        RevokeOutcome::Revoked(row) => Some(row),
        RevokeOutcome::NotGranted => None,
    }
    .unwrap();
    reconstructed.remove(&row.request);
    let table = registry.host_capabilities().full_table();
    assert_eq!(table.len(), reconstructed.len());
    assert_eq!(reconstructed.get(&second), Some(&second_grant));
    assert_eq!(table, vec![second_grant]);
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn scope_matching_is_path_containment_and_exact_host_without_patterns() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("scopes");
    let fs = request(Capability::FsRead {
        root: PathRoot(PathBuf::from("/srv/data")),
    });
    let host = network("api.example.com");
    registry
        .register(meta("scopes", vec![fs.clone(), host.clone()]), FFI.to_vec())
        .unwrap();
    registry
        .host_capabilities()
        .grant(component, fs, provenance("composition", 1))
        .unwrap();
    registry
        .host_capabilities()
        .grant(component, host, provenance("composition", 2))
        .unwrap();
    let checker = registry.capability_checker(component).unwrap();
    assert_eq!(
        checker
            .check(&Capability::FsRead {
                root: PathRoot(PathBuf::from("/srv/data/file.txt")),
            })
            .unwrap(),
        CheckVerdict::Allowed
    );
    let path_denial = denied(
        checker
            .check(&Capability::FsRead {
                root: PathRoot(PathBuf::from("/srv/database/file.txt")),
            })
            .unwrap(),
    )
    .unwrap();
    assert!(!path_denial.declared);
    let host_denial = denied(
        checker
            .check(&Capability::Network {
                host: NetworkHost("sub.api.example.com".to_owned()),
            })
            .unwrap(),
    )
    .unwrap();
    assert_eq!(
        host_denial.scope,
        CapabilityScope::Host("sub.api.example.com".to_owned())
    );
    assert!(!host_denial.declared);
    registry.remove(component).unwrap();
    scheduler.shutdown();
}

#[test]
fn early_start_denial_then_grant_retry_succeeds_without_restart() {
    let scheduler = scheduler();
    let registry = registry(&scheduler);
    let component = id("early-start");
    let need = network("api.example.com");
    registry
        .register(meta("early-start", vec![need.clone()]), FFI.to_vec())
        .unwrap();
    registry.start(component).unwrap();
    let events = registry.subscribe(NonZeroUsize::new(2).unwrap()).unwrap();
    let checker = registry.capability_checker(component).unwrap();
    let denial = denied(checker.check(&need.capability).unwrap()).unwrap();
    assert!(denial.declared);
    let event = events.recv_timeout(TIMEOUT).unwrap();
    assert_eq!(event.kind, LifecycleEventKind::CapabilityDenied(denial));

    registry
        .host_capabilities()
        .grant(component, need.clone(), provenance("boot-composition", 1))
        .unwrap();
    assert_eq!(
        checker.check(&need.capability).unwrap(),
        CheckVerdict::Allowed
    );
    assert_eq!(
        registry.status(component).unwrap().unwrap().state,
        LifecycleState::Running
    );
    assert_eq!(events.try_recv(), Err(EventTryReceiveError::Empty));
    registry.remove(component).unwrap();
    scheduler.shutdown();
}