liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::{
    BTreeMap, BTreeSet, CONTRACT_VERSION, LysSession, MembershipConfig, MembershipRefusal,
    PageBounds, Resource, ServerError, Subject, invalid, malformed,
};

/// The verified subject a membership is asked about: the holder the one
/// `Connect` verification proved, with the directory kind its signed pass
/// states. Only [`Self::from_session`] makes one.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct MembershipSubject {
    pub(super) id: String,
    pub(super) kind: String,
}

impl MembershipSubject {
    /// The subject an admitted Lys session proved.
    ///
    /// # Errors
    /// [`MembershipRefusal::HolderKindUnknown`] when the session cannot name
    /// its holder's kind; [`MembershipRefusal::HolderMalformed`] when the id or
    /// kind is empty or carries control characters.
    pub fn from_session(session: &dyn LysSession) -> Result<Self, MembershipRefusal> {
        let kind = session
            .holder_kind()
            .ok_or(MembershipRefusal::HolderKindUnknown)?;
        let id = session.holder();
        if malformed(id) || malformed(kind) {
            return Err(MembershipRefusal::HolderMalformed);
        }
        Ok(Self {
            id: id.to_owned(),
            kind: kind.to_owned(),
        })
    }

    /// The directory kind, as the signed pass states it.
    #[must_use]
    pub fn kind(&self) -> &str {
        &self.kind
    }

    /// The contract's subject.
    #[must_use]
    pub fn to_contract(&self) -> Subject {
        Subject {
            id: self.id.clone(),
            kind: self.kind.clone(),
        }
    }

    pub(super) fn is(&self, subject: &Subject) -> bool {
        self.id == subject.id && self.kind == subject.kind
    }
}

impl std::fmt::Debug for MembershipSubject {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("MembershipSubject")
            .field("id", &crate::auth_pass::ByteLength(self.id.len()))
            .field("kind", &self.kind)
            .finish()
    }
}

/// A protected channel's placement as the product mapping states it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MappedChannel {
    pub(super) workspace: Resource,
    pub(super) resource: Resource,
}

impl MappedChannel {
    /// The workspace the channel is placed under.
    #[must_use]
    pub const fn workspace(&self) -> &Resource {
        &self.workspace
    }

    /// The placed channel resource.
    #[must_use]
    pub const fn resource(&self) -> &Resource {
        &self.resource
    }
}

/// The validated product mapping.
#[derive(Debug)]
pub(super) struct ChannelMap {
    pub(super) channels: BTreeMap<String, MappedChannel>,
    pub(super) conversations: BTreeMap<u64, String>,
    pub(super) by_resource: BTreeMap<String, String>,
    pub(super) workspaces: BTreeSet<String>,
}

pub(super) fn channel_map(config: &MembershipConfig) -> Result<ChannelMap, ServerError> {
    if config.channels.is_empty() {
        return Err(invalid(
            "auth.lys.membership.channels: at least one protected channel is required",
        ));
    }
    let mut channels = BTreeMap::new();
    let mut by_resource = BTreeMap::new();
    let mut workspaces = BTreeSet::new();
    for entry in &config.channels {
        for (member, value) in [
            ("channel", entry.channel.as_str()),
            ("workspace", entry.workspace.as_str()),
            ("resource", entry.resource.as_str()),
        ] {
            if malformed(value) {
                return Err(invalid(format!(
                    "auth.lys.membership.channels: a {member} is empty or contains control characters"
                )));
            }
        }
        let mapped = MappedChannel {
            workspace: Resource {
                kind: config.workspace_kind.clone(),
                id: entry.workspace.clone(),
            },
            resource: Resource {
                kind: config.channel_kind.clone(),
                id: entry.resource.clone(),
            },
        };
        if channels.insert(entry.channel.clone(), mapped).is_some() {
            return Err(invalid(
                "auth.lys.membership.channels: a channel is listed twice",
            ));
        }
        if by_resource
            .insert(entry.resource.clone(), entry.channel.clone())
            .is_some()
        {
            return Err(invalid(
                "auth.lys.membership.channels: a placed resource is mapped to two channels",
            ));
        }
        workspaces.insert(entry.workspace.clone());
    }
    let mut conversations = BTreeMap::new();
    for entry in &config.conversations {
        if !channels.contains_key(&entry.channel) {
            return Err(invalid(
                "auth.lys.membership.conversations: a conversation names an unlisted channel",
            ));
        }
        if conversations
            .insert(entry.conversation, entry.channel.clone())
            .is_some()
        {
            return Err(invalid(
                "auth.lys.membership.conversations: a conversation is listed twice",
            ));
        }
    }
    Ok(ChannelMap {
        channels,
        conversations,
        by_resource,
        workspaces,
    })
}

pub(super) fn validated_bounds(config: &MembershipConfig) -> Result<PageBounds, ServerError> {
    if config.contract != CONTRACT_VERSION {
        return Err(invalid(format!(
            "auth.lys.membership.contract {} is not served; this liminal speaks {CONTRACT_VERSION}",
            config.contract
        )));
    }
    for (member, value) in [
        ("log_identity", config.log_identity.as_str()),
        ("workspace_kind", config.workspace_kind.as_str()),
        ("channel_kind", config.channel_kind.as_str()),
    ] {
        if malformed(value) {
            return Err(invalid(format!(
                "auth.lys.membership.{member} is empty or contains control characters"
            )));
        }
    }
    if config.workspace_kind == config.channel_kind {
        return Err(invalid(
            "auth.lys.membership: workspace_kind and channel_kind must differ",
        ));
    }
    if config.page_rows == 0 || config.page_bytes == 0 || config.max_decisions == 0 {
        return Err(invalid(
            "auth.lys.membership: page_rows, page_bytes and max_decisions must each be positive",
        ));
    }
    Ok(PageBounds {
        rows: config.page_rows,
        bytes: config.page_bytes,
    })
}