sail-rs 0.5.8

Official Rust SDK for Sail: create and drive sailboxes (sandboxed cloud VMs) with lifecycle, streaming exec, file transfer, and ingress.
Documentation
//! The bound credential objects: the [`Credentials`] entry point obtained
//! from [`Client::credentials`], plus [`Secret`] and
//! [`CredentialInjectionPolicy`], each paired with the [`Client`] that
//! reaches them.
//!
//! Each method is a one-line delegate to the corresponding [`Client`] method,
//! which remains the single implementation (and the surface the language
//! bridges call with plain ids). The delegation means the two surfaces cannot
//! drift: a signature change on either side fails to compile.

use crate::client::Client;
use crate::error::SailError;
use crate::sailbox::object::Sailbox;
use time::OffsetDateTime;

use super::types::{
    CredentialInjectionPolicyInfo, CredentialInjectionPolicyPage, InjectionRule,
    ListCredentialInjectionPoliciesQuery, SecretInfo,
};

/// Secrets and credential injection policies for your organization.
/// Obtained from [`Client::credentials`].
///
/// ```no_run
/// # async fn demo() -> Result<(), sail::error::SailError> {
/// # let client = sail::Client::from_env()?;
/// use sail::InjectionRule;
///
/// let creds = client.credentials();
/// creds.set_secret("GITHUB_TOKEN", "ghp_...").await?;
/// let policy = creds
///     .create_policy(
///         "github",
///         vec![InjectionRule::header(
///             "api.github.com",
///             "Authorization",
///             "Bearer ${secrets.GITHUB_TOKEN}",
///         )],
///     )
///     .await?;
/// let sb = client.sailbox("sb_abc123");
/// sb.set_credential_policy(&policy).await?;
/// # Ok(())
/// # }
/// ```
pub struct Credentials<'a> {
    client: &'a Client,
}

impl std::fmt::Debug for Credentials<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Credentials").finish()
    }
}

impl Credentials<'_> {
    /// Set (create or update) the named secret's value. The value is
    /// write-only: it can be referenced from policy rules as
    /// `${secrets.NAME}` but never read back. Names start with a letter or
    /// number and use letters, numbers, underscores, and dashes (at most 128
    /// characters); values are a single line of text of at most 64 KiB.
    pub async fn set_secret(&self, name: &str, value: &str) -> Result<Secret, SailError> {
        self.client
            .set_secret(name, value)
            .await
            .map(|info| Secret::bind(self.client.clone(), info))
    }

    /// Fetch one secret's metadata (never the value).
    pub async fn get_secret(&self, name: &str) -> Result<Secret, SailError> {
        self.client
            .get_secret(name)
            .await
            .map(|info| Secret::bind(self.client.clone(), info))
    }

    /// List the org's secrets, metadata only.
    pub async fn list_secrets(&self) -> Result<Vec<Secret>, SailError> {
        Ok(self
            .client
            .list_secrets()
            .await?
            .into_iter()
            .map(|info| Secret::bind(self.client.clone(), info))
            .collect())
    }

    /// Delete the named secret. A secret still referenced by a policy cannot
    /// be deleted; the call fails with a 409 [`SailError::Api`] until the
    /// referencing policies are deleted first.
    pub async fn delete_secret(&self, name: &str) -> Result<(), SailError> {
        self.client.delete_secret(name).await
    }

    /// Create a credential injection policy from `rules`. Rules are immutable
    /// after creation; only the name can change later
    /// ([`CredentialInjectionPolicy::rename`]). Every `${secrets.NAME}`
    /// reference must name an existing secret, and each (host, target) pair
    /// may appear at most once.
    pub async fn create_policy(
        &self,
        name: &str,
        rules: Vec<InjectionRule>,
    ) -> Result<CredentialInjectionPolicy, SailError> {
        self.client
            .create_credential_policy(name, &rules)
            .await
            .map(|info| CredentialInjectionPolicy::bind(self.client.clone(), info))
    }

    /// Fetch one policy by id, rules included.
    pub async fn get_policy(
        &self,
        policy_id: &str,
    ) -> Result<CredentialInjectionPolicy, SailError> {
        self.client
            .get_credential_policy(policy_id)
            .await
            .map(|info| CredentialInjectionPolicy::bind(self.client.clone(), info))
    }

    /// List policies with paging and an optional name search. Rows are
    /// summaries (no rules body); fetch a policy by id for its rules.
    pub async fn list_policies(
        &self,
        query: &ListCredentialInjectionPoliciesQuery,
    ) -> Result<CredentialInjectionPolicyPage, SailError> {
        self.client.list_credential_policies(query).await
    }
}

/// A secret stored in your organization. Obtained from
/// [`Credentials::set_secret`], [`Credentials::get_secret`], or
/// [`Credentials::list_secrets`]. The value is write-only and never present
/// here; the object carries the name, timestamps, and [`Secret::delete`].
#[derive(Clone)]
pub struct Secret {
    client: Client,
    info: SecretInfo,
}

impl std::fmt::Debug for Secret {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Secret")
            .field("name", &self.info.name)
            .finish_non_exhaustive()
    }
}

impl Secret {
    pub(crate) fn bind(client: Client, info: SecretInfo) -> Secret {
        Secret { client, info }
    }

    /// The secret's name, unique within the organization.
    pub fn name(&self) -> &str {
        &self.info.name
    }

    /// When the secret was first set.
    pub fn created_at(&self) -> OffsetDateTime {
        self.info.created_at
    }

    /// When the secret's value last changed.
    pub fn updated_at(&self) -> OffsetDateTime {
        self.info.updated_at
    }

    /// The metadata snapshot from the call that produced this object.
    pub fn info(&self) -> &SecretInfo {
        &self.info
    }

    /// Consume the object, keeping just the metadata snapshot.
    pub fn into_info(self) -> SecretInfo {
        self.info
    }

    /// Delete this secret. Same contract as
    /// [`Credentials::delete_secret`], which documents the referenced-secret
    /// conflict.
    pub async fn delete(&self) -> Result<(), SailError> {
        self.client.delete_secret(&self.info.name).await
    }
}

/// A named set of immutable rules that inject values into HTTPS requests
/// your Sailboxes send to matching hosts. Obtained from
/// [`Credentials::create_policy`] or [`Credentials::get_policy`].
#[derive(Clone)]
pub struct CredentialInjectionPolicy {
    client: Client,
    info: CredentialInjectionPolicyInfo,
}

impl std::fmt::Debug for CredentialInjectionPolicy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CredentialInjectionPolicy")
            .field("id", &self.info.id)
            .field("name", &self.info.name)
            .finish_non_exhaustive()
    }
}

impl CredentialInjectionPolicy {
    pub(crate) fn bind(
        client: Client,
        info: CredentialInjectionPolicyInfo,
    ) -> CredentialInjectionPolicy {
        CredentialInjectionPolicy { client, info }
    }

    /// The policy's stable identifier.
    pub fn id(&self) -> &str {
        &self.info.id
    }

    /// The policy's name (the only mutable field).
    pub fn name(&self) -> &str {
        &self.info.name
    }

    /// The policy's rules, immutable after creation.
    pub fn rules(&self) -> &[InjectionRule] {
        &self.info.rules
    }

    /// The data snapshot from the call that produced this object.
    pub fn info(&self) -> &CredentialInjectionPolicyInfo {
        &self.info
    }

    /// Consume the object, keeping just the data snapshot.
    pub fn into_info(self) -> CredentialInjectionPolicyInfo {
        self.info
    }

    /// Rename the policy, updating this object in place. Rules cannot be
    /// changed; create a new policy to change behavior.
    pub async fn rename(&mut self, name: &str) -> Result<(), SailError> {
        self.info = self
            .client
            .rename_credential_policy(&self.info.id, name)
            .await?;
        Ok(())
    }

    /// Delete the policy. A policy attached to any Sailbox cannot be
    /// deleted; the call fails with a 409 [`SailError::Api`] until every
    /// Sailbox detaches or replaces it.
    pub async fn delete(&self) -> Result<(), SailError> {
        self.client.delete_credential_policy(&self.info.id).await
    }
}

// CR-soon aagam: private beta; un-hide together with the module (see lib.rs).
#[doc(hidden)]
impl Client {
    /// The org-credentials surface: secrets and credential injection
    /// policies.
    pub fn credentials(&self) -> Credentials<'_> {
        Credentials { client: self }
    }
}

// CR-soon aagam: private beta; un-hide together with the module (see lib.rs).
#[doc(hidden)]
impl Sailbox {
    // --- the credential-policy attachment slot ---

    /// Attach `policy` to this Sailbox, replacing any previously attached
    /// policy (a Sailbox has at most one). Idempotent. The policy's rules
    /// apply to the Sailbox's outbound HTTPS requests.
    pub async fn set_credential_policy(
        &self,
        policy: &CredentialInjectionPolicy,
    ) -> Result<(), SailError> {
        self.client()
            .set_sailbox_credential_policy(self.sailbox_id(), policy.id())
            .await
    }

    /// The policy currently attached to this Sailbox, or `None`.
    pub async fn credential_policy(&self) -> Result<Option<CredentialInjectionPolicy>, SailError> {
        Ok(self
            .client()
            .sailbox_credential_policy(self.sailbox_id())
            .await?
            .map(|info| CredentialInjectionPolicy::bind(self.client().clone(), info)))
    }

    /// Detach this Sailbox's credential policy. Idempotent: succeeds when no
    /// policy is attached.
    pub async fn clear_credential_policy(&self) -> Result<(), SailError> {
        self.client()
            .clear_sailbox_credential_policy(self.sailbox_id())
            .await
    }
}