alien-core 3.3.29

Deploy software into your customers' cloud accounts and keep it fully managed
Documentation
use crate::{
    ownership_policy_for_resource_type, ResourceEntry, ResourceType, Sandbox, SandboxEgress,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RemoteBindingKind {
    Storage,
    Kv,
    Queue,
    Key,
    Ai,
    Sandbox,
}

/// One resource type's provider-neutral Remote Bindings contract.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RemoteBindingDefinition {
    pub resource_type: &'static str,
    pub permission_set: &'static str,
    pub kind: RemoteBindingKind,
    pub description: &'static str,
    /// Setup-owned parent resources that this binding kind may require. They do not turn a
    /// bindings-only stack into an application stack.
    pub setup_support_resource_types: &'static [&'static str],
    /// Increment when the permission set's effective grants change. This makes direct setup
    /// updates reconcile permissions even when the application resource config is unchanged.
    pub revision: u32,
}

const DEFINITIONS: &[RemoteBindingDefinition] = &[
    RemoteBindingDefinition {
        resource_type: "storage",
        permission_set: "storage/remote-data-write",
        kind: RemoteBindingKind::Storage,
        description: "Read and write objects in this storage resource",
        setup_support_resource_types: &[
            "azure_resource_group",
            "azure_storage_account",
            "service_activation",
        ],
        revision: 1,
    },
    RemoteBindingDefinition {
        resource_type: "queue",
        permission_set: "queue/publish",
        kind: RemoteBindingKind::Queue,
        description: "Send messages to this queue",
        setup_support_resource_types: &["azure_resource_group", "azure_service_bus_namespace", "service_activation"],
        revision: 1,
    },
    RemoteBindingDefinition {
        resource_type: "kv",
        permission_set: "kv/remote-data-write",
        kind: RemoteBindingKind::Kv,
        description: "Read and write entries in this key-value store",
        setup_support_resource_types: &[
            "azure_resource_group",
            "azure_storage_account",
            "service_activation",
        ],
        revision: 1,
    },
    RemoteBindingDefinition {
        resource_type: "key",
        permission_set: "key/remote-cryptography",
        kind: RemoteBindingKind::Key,
        description: "Encrypt and decrypt small values with this key",
        setup_support_resource_types: &["azure_resource_group", "service_activation"],
        revision: 1,
    },
    RemoteBindingDefinition {
        resource_type: "ai",
        permission_set: "ai/invoke",
        kind: RemoteBindingKind::Ai,
        description: "Invoke models through this AI resource",
        setup_support_resource_types: &["azure_resource_group", "service_activation"],
        revision: 1,
    },
    RemoteBindingDefinition {
        resource_type: "sandbox",
        permission_set: "sandbox/remote-execute",
        kind: RemoteBindingKind::Sandbox,
        description:
            "Create and terminate sandboxes in this sandbox resource, and run arbitrary code inside them",
        // A sandbox's parent is the MicroVM image its own emitter builds, and an open-egress
        // sandbox attaches no VPC connector, so setup owes this binding no other resource.
        setup_support_resource_types: &[],
        revision: 1,
    },
];

pub fn remote_binding_definition(
    resource_type: &ResourceType,
) -> Option<&'static RemoteBindingDefinition> {
    DEFINITIONS
        .iter()
        .find(|definition| definition.resource_type == resource_type.as_ref())
}

/// A grant is attached by the setup artifact, so only a resource it renders something for can
/// be published: every Frozen one, and the Live sandbox through its scaffolding.
pub fn remote_binding_for_entry(entry: &ResourceEntry) -> Option<&'static RemoteBindingDefinition> {
    let resource_type = entry.config.resource_type();
    (entry.remote_access
        && ownership_policy_for_resource_type(resource_type.as_ref())
            .emits_setup_scaffolding(entry.lifecycle))
    .then(|| remote_binding_definition(&resource_type))
    .flatten()
}

/// Why a declaration's remote binding is one a deployment cannot deliver, if it cannot.
///
/// Two cases, both sandbox-only and both about a declared policy the remote grant cannot carry.
///
/// **Egress.** The same refusal on every cloud that publishes a sandbox remotely, for mechanisms
/// that are worth telling apart. On AWS the declared connector is *unreachable*: starting a
/// sandbox is additionally authorized as `lambda:PassNetworkConnector` and the remote grant
/// passes only AWS's own connectors. On Azure it is *bypassable*: the grant is the
/// `SandboxGroup Data Owner` data-plane role, so its holder creates sandboxes against the group
/// directly and the provider that would have applied the declared policy never runs. The Azure
/// case is the security-relevant one — it is inherent to handing out a data-plane role, not a
/// gap in an implementation that could later close it. On GCP the policy lives on the environment
/// template, which the remote grant carries no verb to create or replace.
///
/// **Preview ports.** AWS's `CreateMicrovmAuthToken` has no port condition key, so a declared
/// list bounds a caller going through the provider but not a holder of the leased credentials —
/// a bound that only looks like one. On Azure and GCP this branch is unreachable rather than
/// merely unused: `preview` is false for both, so `validate_capabilities` refuses a non-empty
/// list at plan time before a stack gets this far.
///
/// Preflight refuses either; emitters and generated docs read this so nothing advertises a grant
/// that cannot be used.
pub fn remote_binding_undeliverable_reason(entry: &ResourceEntry) -> Option<&'static str> {
    remote_binding_for_entry(entry)?;
    let sandbox = entry.config.downcast_ref::<Sandbox>()?;

    if sandbox.privileged_supervisor.is_some() {
        return Some("a remotely published sandbox cannot declare privilegedSupervisor; the raw grant can start retained image versions with a different command identity or egress policy; use an ordinary workload binding");
    }

    if !matches!(sandbox.egress, SandboxEgress::Allow) {
        return Some(
            "a remotely published sandbox must declare egress 'allow'; the remote grant either \
             cannot pass a declared connector or lets its holder create sandboxes that ignore the \
             declared policy, so the declaration would not bound the remote caller",
        );
    }

    if !sandbox.preview_ports.is_empty() {
        return Some(
            "a remotely published sandbox must declare no previewPorts; the sandbox token mint \
             carries no port condition, so the list bounds a caller reaching the sandbox through \
             its binding but not a holder of the remote credentials",
        );
    }

    None
}

/// Whether a declaration's remote binding is one a deployment can actually deliver.
pub fn remote_binding_is_deliverable(entry: &ResourceEntry) -> bool {
    remote_binding_undeliverable_reason(entry).is_none()
}

/// Whether a stack's remote bindings mean this global management set belongs to the caller's
/// identity rather than the deployment's.
///
/// The binding's own set always does. A sandbox binding additionally claims anything that reaches
/// a sandbox, because the remote caller drives those; `reaches_a_sandbox` decides that, so the
/// permission registry stays the single place the verbs are named.
pub fn remote_binding_claims_management_set<'a>(
    resources: impl IntoIterator<Item = &'a ResourceEntry>,
    permission_set_id: &str,
    reaches_a_sandbox: impl Fn() -> bool,
) -> bool {
    resources.into_iter().any(|entry| {
        remote_binding_for_entry(entry).is_some_and(|definition| {
            permission_set_id == definition.permission_set
                || (definition.kind == RemoteBindingKind::Sandbox && reaches_a_sandbox())
        })
    })
}

pub fn remote_binding_definitions() -> &'static [RemoteBindingDefinition] {
    DEFINITIONS
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{ResourceLifecycle, Sandbox, SandboxCode, SandboxLifecyclePolicy, SandboxLimits};

    fn remote_sandbox(egress: SandboxEgress, preview_ports: Vec<u16>) -> ResourceEntry {
        let sandbox = Sandbox::new("agent-sbx".to_string())
            .code(SandboxCode::Image {
                image: "ubuntu".to_string(),
            })
            .limits(SandboxLimits {
                cpu: "1".to_string(),
                memory: "2Gi".to_string(),
                disk: "20Gi".to_string(),
                max_processes: None,
            })
            .egress(egress)
            .lifecycle(SandboxLifecyclePolicy {
                max_lifetime_seconds: None,
                idle_pause_seconds: None,
            })
            .preview_ports(preview_ports)
            .build();

        ResourceEntry {
            enabled_when: None,
            config: crate::Resource::new(sandbox),
            dependencies: Vec::new(),
            lifecycle: ResourceLifecycle::Frozen,
            remote_access: true,
        }
    }

    #[test]
    fn a_remote_grant_cannot_bypass_supervision_through_a_retained_version() {
        for egress in [
            SandboxEgress::Allow,
            SandboxEgress::Deny,
            SandboxEgress::AllowDomains {
                domains: vec!["example.com".to_string()],
            },
        ] {
            let mut entry = remote_sandbox(egress, vec![]);
            let mut sandbox = entry
                .config
                .downcast_ref::<Sandbox>()
                .expect("sandbox")
                .clone();
            sandbox.privileged_supervisor =
                Some(crate::SandboxPrivilegedSupervisor { command_uid: 60001 });
            entry.config = crate::Resource::new(sandbox);
            assert!(remote_binding_undeliverable_reason(&entry)
                .expect("raw version-wide grant is unsafe")
                .contains("privilegedSupervisor"));
            entry.remote_access = false;
            assert_eq!(
                remote_binding_undeliverable_reason(&entry),
                None,
                "ordinary bindings select the active version"
            );
        }
    }

    /// Every deployment today declares no ports; a refusal that caught them would be the worst
    /// outcome of adding one.
    #[test]
    fn a_remote_sandbox_declaring_no_ports_is_deliverable() {
        assert!(remote_binding_is_deliverable(&remote_sandbox(
            SandboxEgress::Allow,
            Vec::new()
        )));
    }

    /// The mint carries no port condition key, so the list bounds a caller reaching the sandbox
    /// through its binding and not a holder of the leased credentials.
    #[test]
    fn a_remote_sandbox_declaring_ports_is_refused() {
        let reason =
            remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
                .expect("a declared port list is not deliverable to a remote caller");

        assert!(
            reason.contains("previewPorts"),
            "the refusal must name the field the user declared"
        );
    }

    /// The question only applies to a remote binding. A deployment's own compute reaching its own
    /// sandbox is not this problem, and refusing it would be a false positive.
    #[test]
    fn a_sandbox_with_no_remote_binding_may_declare_ports() {
        let mut entry = remote_sandbox(SandboxEgress::Allow, vec![8080]);
        entry.remote_access = false;

        assert_eq!(remote_binding_undeliverable_reason(&entry), None);
        assert!(remote_binding_is_deliverable(&entry));
    }

    /// Two undeliverable declarations, two reasons. Collapsing them would answer a port mistake
    /// with an egress instruction.
    #[test]
    fn each_undeliverable_declaration_answers_in_its_own_terms() {
        let egress =
            remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Deny, Vec::new()))
                .expect("a restricted egress is not deliverable");
        let ports =
            remote_binding_undeliverable_reason(&remote_sandbox(SandboxEgress::Allow, vec![8080]))
                .expect("a declared port list is not deliverable");

        assert_ne!(egress, ports, "one reason cannot stand in for the other");
        assert!(egress.contains("egress"));
    }
}