lenso-secrets-env-plugin 0.1.3

Allowlisted environment-backed Secrets Provider Plugin for Lenso vNext.
Documentation
use std::{cell::RefCell, collections::BTreeMap, rc::Rc, time::Duration};

use lenso_app_plan::{
    AppComposition, CapabilityBinding, CapabilityEndpointPlan, CapabilityRequirementPlan,
    PluginInstancePlan, ResolvedAppPlan,
};
use lenso_capability_secrets::{
    CAPABILITY_ID, DESCRIPTOR_VERSION, RESOLVE_OPERATION, ResolveError, ResolveRequest, Secrets,
};
use lenso_kernel::{DeterministicDriver, Kernel, RuntimeFailure, ShutdownOutcome};
use lenso_native_adapter::{
    NativePluginFactory, NativePluginFactoryContext, NativePluginInstance, NativePluginRegistry,
};

use super::{
    EnvSecretsConfig, EnvSecretsConfigError, EnvSecretsFactory, PACKAGE_ID, PLUGIN_DESCRIPTOR_JSON,
    SecretSource, SourceUnavailable,
};

const CALLER_PACKAGE_ID: &str = "test.secrets-caller";

#[derive(Debug)]
struct CallerFactory;

impl NativePluginFactory for CallerFactory {
    fn package_id(&self) -> &'static str {
        CALLER_PACKAGE_ID
    }

    fn instantiate(
        &self,
        _context: NativePluginFactoryContext<'_>,
    ) -> Result<NativePluginInstance, RuntimeFailure> {
        Ok(NativePluginInstance::default())
    }
}

#[derive(Clone, Debug, Default)]
struct MutableSource {
    values: Rc<RefCell<BTreeMap<String, String>>>,
}

impl MutableSource {
    fn insert(&self, name: &str, value: &str) {
        self.values
            .borrow_mut()
            .insert(name.to_owned(), value.to_owned());
    }

    fn remove(&self, name: &str) {
        self.values.borrow_mut().remove(name);
    }
}

impl SecretSource for MutableSource {
    fn read(&self, environment_variable: &str) -> Result<String, SourceUnavailable> {
        self.values
            .borrow()
            .get(environment_variable)
            .cloned()
            .ok_or(SourceUnavailable)
    }
}

fn config() -> EnvSecretsConfig {
    EnvSecretsConfig::new()
        .with_reference("database/url", "APP_DATABASE_URL")
        .expect("test mapping should be valid")
}

#[test]
fn package_descriptor_and_linked_factory_are_derived_from_the_plugin() {
    let descriptor: serde_json::Value =
        serde_json::from_str(PLUGIN_DESCRIPTOR_JSON).expect("descriptor should be valid JSON");
    assert_eq!(descriptor["plugin_id"], PACKAGE_ID);
    assert_eq!(
        descriptor["provided_capabilities"][0]["capability_id"],
        CAPABILITY_ID
    );
    assert_eq!(
        descriptor["configuration_schema"]["required"],
        serde_json::json!(["references"])
    );

    let linked = NativePluginRegistry::new()
        .with_linked_factories()
        .factories()
        .filter(|factory| factory.package_id() == PACKAGE_ID)
        .count();
    assert_eq!(linked, 1);
}

fn plan(config: &EnvSecretsConfig) -> ResolvedAppPlan {
    plan_with_configuration(serde_json::to_string(config).expect("config should serialize"))
}

fn plan_with_configuration(configuration: String) -> ResolvedAppPlan {
    let caller = PluginInstancePlan::new("caller", CALLER_PACKAGE_ID).with_requirement(
        CapabilityRequirementPlan::one(CAPABILITY_ID, DESCRIPTOR_VERSION),
    );
    let secrets = PluginInstancePlan::new("secrets", PACKAGE_ID)
        .with_configuration(configuration)
        .with_capability(CapabilityEndpointPlan::new(
            CAPABILITY_ID,
            DESCRIPTOR_VERSION,
            [RESOLVE_OPERATION],
        ));
    AppComposition::new(
        vec![caller, secrets],
        vec![CapabilityBinding::new(
            "caller",
            CAPABILITY_ID,
            DESCRIPTOR_VERSION,
            "secrets",
        )],
    )
    .resolve()
    .expect("Secrets test Composition should resolve")
}

#[test]
fn invalid_plan_configuration_fails_before_preparation() {
    let driver = DeterministicDriver::new();
    let result = driver.run(Kernel::start_native(
        plan_with_configuration(
            serde_json::json!({"references": {"../database": "APP_DATABASE_URL"}}).to_string(),
        ),
        driver.clone(),
        NativePluginRegistry::new()
            .with_factory(CallerFactory)
            .with_factory(EnvSecretsFactory::with_source(Rc::new(
                MutableSource::default(),
            ))),
    ));

    assert!(matches!(
        result,
        Err(RuntimeFailure::InvalidResolvedPlan { detail })
            if detail.contains("invalid logical secret reference")
    ));
}

#[test]
fn duplicate_plan_references_are_rejected_instead_of_overwritten() {
    let driver = DeterministicDriver::new();
    let result = driver.run(Kernel::start_native(
        plan_with_configuration(
            r#"{"references":{"database/url":"FIRST_URL","database/url":"SECOND_URL"}}"#.to_owned(),
        ),
        driver.clone(),
        NativePluginRegistry::new()
            .with_factory(CallerFactory)
            .with_factory(EnvSecretsFactory::with_source(Rc::new(
                MutableSource::default(),
            ))),
    ));

    assert!(matches!(
        result,
        Err(RuntimeFailure::InvalidResolvedPlan { detail })
            if detail.contains("duplicate logical secret reference")
    ));
}

#[test]
fn configured_secret_resolves_and_source_rotation_is_observed() {
    let source = MutableSource::default();
    source.insert("APP_DATABASE_URL", "postgres://first");
    let config = config();
    let factory = EnvSecretsFactory::with_source(Rc::new(source.clone()));
    let driver = DeterministicDriver::new();
    let app = driver
        .run(Kernel::start_native(
            plan(&config),
            driver.clone(),
            NativePluginRegistry::new()
                .with_factory(CallerFactory)
                .with_factory(factory),
        ))
        .expect("configured source should prepare the App");

    let first = driver
        .run(app.invoke::<Secrets>(
            "caller",
            RESOLVE_OPERATION,
            ResolveRequest {
                reference: "database/url".to_owned(),
            },
        ))
        .expect("resolved binding should be available")
        .expect("configured reference should resolve");
    assert_eq!(first.value, "postgres://first");
    assert!(!format!("{first:?}").contains("postgres://first"));

    source.insert("APP_DATABASE_URL", "postgres://rotated");
    let rotated = driver
        .run(app.invoke::<Secrets>(
            "caller",
            RESOLVE_OPERATION,
            ResolveRequest {
                reference: "database/url".to_owned(),
            },
        ))
        .expect("resolved binding should stay available")
        .expect("configured reference should resolve after rotation");
    assert_eq!(rotated.value, "postgres://rotated");
    assert_eq!(
        driver.run(app.shutdown(Duration::from_secs(1))),
        ShutdownOutcome::Clean
    );
}

#[test]
fn invalid_and_unknown_references_are_distinct_domain_errors() {
    let source = MutableSource::default();
    source.insert("APP_DATABASE_URL", "secret-value");
    let config = config();
    let driver = DeterministicDriver::new();
    let app = driver
        .run(Kernel::start_native(
            plan(&config),
            driver.clone(),
            NativePluginRegistry::new()
                .with_factory(CallerFactory)
                .with_factory(EnvSecretsFactory::with_source(Rc::new(source))),
        ))
        .unwrap();

    let invalid = driver
        .run(app.invoke::<Secrets>(
            "caller",
            RESOLVE_OPERATION,
            ResolveRequest {
                reference: "../database".to_owned(),
            },
        ))
        .unwrap()
        .unwrap_err();
    assert_eq!(invalid, ResolveError::InvalidReference);
    let unknown = driver
        .run(app.invoke::<Secrets>(
            "caller",
            RESOLVE_OPERATION,
            ResolveRequest {
                reference: "payments/key".to_owned(),
            },
        ))
        .unwrap()
        .unwrap_err();
    assert_eq!(unknown, ResolveError::UnknownReference);
    assert_eq!(
        driver.run(app.shutdown(Duration::from_secs(1))),
        ShutdownOutcome::Clean
    );
}

#[test]
fn missing_required_source_fails_preparation_without_leaking_source_details() {
    let driver = DeterministicDriver::new();
    let config = config();
    let result = driver.run(Kernel::start_native(
        plan(&config),
        driver.clone(),
        NativePluginRegistry::new()
            .with_factory(CallerFactory)
            .with_factory(EnvSecretsFactory::with_source(Rc::new(
                MutableSource::default(),
            ))),
    ));

    assert!(matches!(
        result,
        Err(RuntimeFailure::PluginFailure { detail })
            if detail.contains("database/url")
                && !detail.contains("APP_DATABASE_URL")
                && !detail.contains("secret-value")
    ));
}

#[test]
fn runtime_source_loss_is_truthful_and_does_not_fallback() {
    let source = MutableSource::default();
    source.insert("APP_DATABASE_URL", "secret-value");
    let config = config();
    let driver = DeterministicDriver::new();
    let app = driver
        .run(Kernel::start_native(
            plan(&config),
            driver.clone(),
            NativePluginRegistry::new()
                .with_factory(CallerFactory)
                .with_factory(EnvSecretsFactory::with_source(Rc::new(source.clone()))),
        ))
        .unwrap();
    source.remove("APP_DATABASE_URL");

    let error = driver
        .run(app.invoke::<Secrets>(
            "caller",
            RESOLVE_OPERATION,
            ResolveRequest {
                reference: "database/url".to_owned(),
            },
        ))
        .expect_err("lost source must be a Runtime Failure");
    assert!(matches!(
        error,
        RuntimeFailure::PluginFailure { detail }
            if detail.contains("database/url") && !detail.contains("secret-value")
    ));
}

#[test]
fn configuration_rejects_invalid_or_duplicate_mappings() {
    let mut config = EnvSecretsConfig::new();
    assert_eq!(config.validate(), Err(EnvSecretsConfigError::Empty));
    assert_eq!(
        config.insert("../database", "APP_DATABASE_URL"),
        Err(EnvSecretsConfigError::InvalidReference)
    );
    assert_eq!(
        config.insert("database/url", "not-portable-name"),
        Err(EnvSecretsConfigError::InvalidEnvironmentVariable)
    );
    config.insert("database/url", "APP_DATABASE_URL").unwrap();
    assert_eq!(
        config.insert("database/url", "OTHER_DATABASE_URL"),
        Err(EnvSecretsConfigError::DuplicateReference)
    );
    assert_eq!(config.len(), 1);
    assert!(!config.is_empty());
}