liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
use super::{
    Arc, Decided, LysSession, MembershipAction, MembershipGate, MembershipRefusal,
    MembershipSubject, bump,
};

/// One connection's membership binding (ACCESS-006 R1, R6): the gate, the
/// connection's id and the verified subject from its one `Connect`, or the
/// exact reason it has none.
///
/// Dropped with the connection's state on every
/// teardown path, releasing the bindings it held.
#[derive(Debug, Default)]
pub struct ConnectionMembership {
    pub(super) bound: Option<BoundMembership>,
}

#[derive(Debug)]
pub(super) struct BoundMembership {
    pub(super) gate: Arc<MembershipGate>,
    pub(super) pid: u64,
    pub(super) holder: Result<MembershipSubject, MembershipRefusal>,
}

impl ConnectionMembership {
    /// Binds connection `pid` at `Connect`: the subject is the admitted Lys
    /// `session`'s holder, or none. Without a gate nothing is protected.
    #[must_use]
    pub fn bind(
        gate: Option<Arc<MembershipGate>>,
        pid: u64,
        session: Option<&dyn LysSession>,
    ) -> Self {
        Self {
            bound: gate.map(|gate| BoundMembership {
                gate,
                pid,
                holder: session.map_or(
                    Err(MembershipRefusal::HolderUnverified),
                    MembershipSubject::from_session,
                ),
            }),
        }
    }

    /// The gate, when one is bound.
    #[must_use]
    pub fn gate(&self) -> Option<&Arc<MembershipGate>> {
        self.bound.as_ref().map(|bound| &bound.gate)
    }

    /// The verified subject, or why there is none.
    ///
    /// # Errors
    /// [`MembershipRefusal::HolderUnverified`] without a gate or a holder;
    /// the subject's own refusal otherwise.
    pub fn holder(&self) -> Result<&MembershipSubject, MembershipRefusal> {
        self.bound
            .as_ref()
            .ok_or(MembershipRefusal::HolderUnverified)
            .and_then(|bound| bound.holder.as_ref().map_err(Clone::clone))
    }

    /// Scoped guest admission for this connection's holder at `Connect`.
    ///
    /// # Errors
    /// As for [`MembershipGate::admit_connect`]; the holder's own refusal.
    pub fn admit_connect(&self) -> Result<Option<u64>, MembershipRefusal> {
        let Some(bound) = &self.bound else {
            return Ok(None);
        };
        if bound.gate.connect_workspace.is_none() {
            return Ok(None);
        }
        let holder = bound.holder.as_ref().map_err(Clone::clone)?;
        bound.gate.admit_connect(holder)
    }

    /// Decides `action` on `channel` for this connection.
    ///
    /// # Errors
    /// As for [`MembershipGate::check`]; a protected channel on a connection
    /// with no verified subject refuses with that subject's cause.
    pub fn check(
        &self,
        channel: &str,
        action: MembershipAction,
    ) -> Result<Decided, MembershipRefusal> {
        self.decide(channel, action, false)
    }

    /// [`Self::check`] at a final data-release seam.
    ///
    /// # Errors
    /// As for [`Self::check`].
    pub fn release(
        &self,
        channel: &str,
        action: MembershipAction,
    ) -> Result<Decided, MembershipRefusal> {
        self.decide(channel, action, true)
    }

    /// [`Self::release`] for a participant conversation, on the channel the
    /// mapping places it under; an unmapped conversation is not protected.
    ///
    /// # Errors
    /// As for [`Self::check`].
    pub fn release_conversation(&self, conversation: u64) -> Result<Decided, MembershipRefusal> {
        let Some(bound) = &self.bound else {
            return Ok(Decided::NotProtected);
        };
        let Some(channel) = bound.gate.channel_for_conversation(conversation) else {
            return Ok(Decided::NotProtected);
        };
        self.decide(channel, MembershipAction::Read, true)
    }

    pub(super) fn decide(
        &self,
        channel: &str,
        action: MembershipAction,
        release: bool,
    ) -> Result<Decided, MembershipRefusal> {
        let Some(bound) = &self.bound else {
            return Ok(Decided::NotProtected);
        };
        if !bound.gate.protects(channel) {
            return Ok(Decided::NotProtected);
        }
        let holder = match &bound.holder {
            Ok(holder) => holder,
            Err(refusal) => {
                bump(&bound.gate.counters.refused);
                return Err(refusal.clone());
            }
        };
        if release {
            bound
                .gate
                .check_release(bound.pid, Some(holder), channel, action)
        } else {
            bound.gate.check(bound.pid, Some(holder), channel, action)
        }
    }
}

impl ConnectionMembership {
    /// The final release check a queued delivery owner carries for
    /// `action` on the protected `channel` (ACCESS-006 R3): `None` when the
    /// channel is not protected or the connection has no verified subject
    /// (its admission was already refused, so no such owner exists).
    #[must_use]
    pub fn release_check(&self, channel: &str, action: MembershipAction) -> Option<ReleaseCheck> {
        let bound = self.bound.as_ref()?;
        if !bound.gate.protects(channel) {
            return None;
        }
        let holder = bound.holder.as_ref().ok()?;
        Some(ReleaseCheck {
            gate: Arc::clone(&bound.gate),
            pid: bound.pid,
            holder: holder.clone(),
            channel: channel.to_owned(),
            action,
        })
    }
}

/// The membership check a queued delivery owner carries to its final release
/// (ACCESS-006 R3).
///
/// It is the same gate, connection, verified subject, channel and action as
/// its admission, decided again at release against the current generation,
/// so a deny applied after admission releases nothing.
#[derive(Clone, Debug)]
pub struct ReleaseCheck {
    pub(super) gate: Arc<MembershipGate>,
    pub(super) pid: u64,
    pub(super) holder: MembershipSubject,
    pub(super) channel: String,
    pub(super) action: MembershipAction,
}

impl ReleaseCheck {
    /// Decides the release now.
    ///
    /// # Errors
    /// As for [`MembershipGate::check`].
    pub fn check(&self) -> Result<Decided, MembershipRefusal> {
        self.gate
            .check_release(self.pid, Some(&self.holder), &self.channel, self.action)
    }
}

impl Drop for ConnectionMembership {
    fn drop(&mut self) {
        if let Some(bound) = &self.bound {
            bound.gate.release_connection(bound.pid);
        }
    }
}