sim-lib-operation-gate 0.1.0

Domain-neutral capability and exact-approval gate for effectful SIM operations.
Documentation
//! Operation-gate conformance tests.

use super::*;
use sim_kernel::{
    Symbol,
    effect::{effect_abort_op_key, effect_resume_op_key},
    testing::bare_cx,
};
use std::cell::{Cell, RefCell};

struct Verifier(bool);
impl ApprovalVerifier for Verifier {
    fn verify(&self, _: &Approval) -> Result<()> {
        if self.0 {
            Ok(())
        } else {
            Err(Error::Eval("invalid or expired approval".into()))
        }
    }
}
#[derive(Default)]
struct Uses(Cell<usize>);
impl ApprovalUse for Uses {
    fn consume(&self, _: &Approval) -> Result<()> {
        self.0.set(self.0.get() + 1);
        Ok(())
    }
}
#[derive(Default)]
struct Sink(RefCell<Vec<GateRecord>>);
impl GateRecordSink for Sink {
    fn record(&self, record: GateRecord) -> Result<()> {
        self.0.borrow_mut().push(record);
        Ok(())
    }
}

fn declaration(mode: ExecutionMode) -> OperationDeclaration {
    OperationDeclaration {
        operation: "fixture/run".into(),
        subject: Ref::Symbol(Symbol::qualified("fixture", "one")),
        capability: CapabilityName::new("fixture/run"),
        mode,
    }
}
fn effect(cx: &mut Cx, declaration: &OperationDeclaration) -> Effect {
    Effect::new(
        cx.fresh_handle(),
        Symbol::qualified("fixture", "operation"),
        declaration.subject.clone(),
        Ref::Symbol(Symbol::new("input")),
        Ref::Symbol(Symbol::qualified("core", "Any")),
        effect_resume_op_key(),
        effect_abort_op_key(),
    )
    .requiring(declaration.capability.clone())
}

#[test]
fn exact_subject_decision_and_verifier_fail_closed() {
    for (approval, verifier, expected) in [
        (
            Approval {
                id: "a".into(),
                subject: Ref::Symbol(Symbol::qualified("fixture", "wrong")),
                decision: ApprovalDecision::Approve,
            },
            true,
            "subject",
        ),
        (
            Approval {
                id: "a".into(),
                subject: declaration(ExecutionMode::Reviewed).subject,
                decision: ApprovalDecision::Deny,
            },
            true,
            "does not approve",
        ),
        (
            Approval {
                id: "a".into(),
                subject: declaration(ExecutionMode::Reviewed).subject,
                decision: ApprovalDecision::Approve,
            },
            false,
            "expired",
        ),
    ] {
        let mut cx = bare_cx();
        cx.grant_named("fixture/run");
        let d = declaration(ExecutionMode::Reviewed);
        let e = effect(&mut cx, &d);
        let uses = Uses::default();
        let sink = Sink::default();
        let error = guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: Some(&approval),
                verifier: &Verifier(verifier),
                approval_use: &uses,
                sink: &sink,
                sink_failure: SinkFailurePolicy::FailClosed,
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok"))),
        )
        .unwrap_err();
        assert!(error.to_string().contains(expected));
        assert_eq!(uses.0.get(), 0);
    }
}

#[test]
fn observation_and_recorded_need_no_approval_but_reviewed_does() {
    for mode in [ExecutionMode::Observation, ExecutionMode::Recorded] {
        let mut cx = bare_cx();
        cx.grant_named("fixture/run");
        let d = declaration(mode);
        let e = effect(&mut cx, &d);
        let uses = Uses::default();
        let sink = Sink::default();
        guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: None,
                verifier: &Verifier(true),
                approval_use: &uses,
                sink: &sink,
                sink_failure: SinkFailurePolicy::FailClosed,
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok"))),
        )
        .unwrap();
        assert_eq!(uses.0.get(), 0);
        assert_eq!(sink.0.borrow()[0].mode, mode);
    }
}

struct DeniedUse;
impl ApprovalUse for DeniedUse {
    fn consume(&self, _: &Approval) -> Result<()> {
        Err(Error::Eval("approval use denied".into()))
    }
}
struct FailingSink;
impl GateRecordSink for FailingSink {
    fn record(&self, _: GateRecord) -> Result<()> {
        Err(Error::Eval("sink failed".into()))
    }
}

#[test]
fn capability_use_performer_and_sink_failures_are_observable() {
    let d = declaration(ExecutionMode::Recorded);
    let mut cx = bare_cx();
    let e = effect(&mut cx, &d);
    let uses = Uses::default();
    let sink = Sink::default();
    assert!(
        guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: None,
                verifier: &Verifier(true),
                approval_use: &uses,
                sink: &sink,
                sink_failure: SinkFailurePolicy::FailClosed
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok")))
        )
        .unwrap_err()
        .to_string()
        .contains("capability")
    );

    let approval = Approval {
        id: "a".into(),
        subject: d.subject.clone(),
        decision: ApprovalDecision::Approve,
    };
    let reviewed = declaration(ExecutionMode::Reviewed);
    let mut cx = bare_cx();
    cx.grant_named("fixture/run");
    let e = effect(&mut cx, &reviewed);
    assert!(
        guard_operation(
            &mut cx,
            &reviewed,
            e,
            GateContext {
                approval: Some(&approval),
                verifier: &Verifier(true),
                approval_use: &DeniedUse,
                sink: &sink,
                sink_failure: SinkFailurePolicy::FailClosed
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok")))
        )
        .unwrap_err()
        .to_string()
        .contains("use denied")
    );

    let mut cx = bare_cx();
    cx.grant_named("fixture/run");
    let e = effect(&mut cx, &d);
    assert!(
        guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: None,
                verifier: &Verifier(true),
                approval_use: &uses,
                sink: &sink,
                sink_failure: SinkFailurePolicy::FailClosed
            },
            |_, _| Err(Error::Eval("performer failed".into()))
        )
        .unwrap_err()
        .to_string()
        .contains("performer failed")
    );

    let mut cx = bare_cx();
    cx.grant_named("fixture/run");
    let e = effect(&mut cx, &d);
    assert!(
        guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: None,
                verifier: &Verifier(true),
                approval_use: &uses,
                sink: &FailingSink,
                sink_failure: SinkFailurePolicy::FailClosed
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok")))
        )
        .unwrap_err()
        .to_string()
        .contains("sink failed")
    );
    let mut cx = bare_cx();
    cx.grant_named("fixture/run");
    let e = effect(&mut cx, &d);
    assert_eq!(
        guard_operation(
            &mut cx,
            &d,
            e,
            GateContext {
                approval: None,
                verifier: &Verifier(true),
                approval_use: &uses,
                sink: &FailingSink,
                sink_failure: SinkFailurePolicy::PreserveResult
            },
            |_, _| Ok(Ref::Symbol(Symbol::new("ok")))
        )
        .unwrap(),
        Ref::Symbol(Symbol::new("ok"))
    );
}