Skip to main content

liminal_server/auth_membership/
connection.rs

1use super::{
2    Arc, Decided, LysSession, MembershipAction, MembershipGate, MembershipRefusal,
3    MembershipSubject, bump,
4};
5
6/// One connection's membership binding (ACCESS-006 R1, R6): the gate, the
7/// connection's id and the verified subject from its one `Connect`, or the
8/// exact reason it has none.
9///
10/// Dropped with the connection's state on every
11/// teardown path, releasing the bindings it held.
12#[derive(Debug, Default)]
13pub struct ConnectionMembership {
14    pub(super) bound: Option<BoundMembership>,
15}
16
17#[derive(Debug)]
18pub(super) struct BoundMembership {
19    pub(super) gate: Arc<MembershipGate>,
20    pub(super) pid: u64,
21    pub(super) holder: Result<MembershipSubject, MembershipRefusal>,
22}
23
24impl ConnectionMembership {
25    /// Binds connection `pid` at `Connect`: the subject is the admitted Lys
26    /// `session`'s holder, or none. Without a gate nothing is protected.
27    #[must_use]
28    pub fn bind(
29        gate: Option<Arc<MembershipGate>>,
30        pid: u64,
31        session: Option<&dyn LysSession>,
32    ) -> Self {
33        Self {
34            bound: gate.map(|gate| BoundMembership {
35                gate,
36                pid,
37                holder: session.map_or(
38                    Err(MembershipRefusal::HolderUnverified),
39                    MembershipSubject::from_session,
40                ),
41            }),
42        }
43    }
44
45    /// The gate, when one is bound.
46    #[must_use]
47    pub fn gate(&self) -> Option<&Arc<MembershipGate>> {
48        self.bound.as_ref().map(|bound| &bound.gate)
49    }
50
51    /// The verified subject, or why there is none.
52    ///
53    /// # Errors
54    /// [`MembershipRefusal::HolderUnverified`] without a gate or a holder;
55    /// the subject's own refusal otherwise.
56    pub fn holder(&self) -> Result<&MembershipSubject, MembershipRefusal> {
57        self.bound
58            .as_ref()
59            .ok_or(MembershipRefusal::HolderUnverified)
60            .and_then(|bound| bound.holder.as_ref().map_err(Clone::clone))
61    }
62
63    /// Scoped guest admission for this connection's holder at `Connect`.
64    ///
65    /// # Errors
66    /// As for [`MembershipGate::admit_connect`]; the holder's own refusal.
67    pub fn admit_connect(&self) -> Result<Option<u64>, MembershipRefusal> {
68        let Some(bound) = &self.bound else {
69            return Ok(None);
70        };
71        if bound.gate.connect_workspace.is_none() {
72            return Ok(None);
73        }
74        let holder = bound.holder.as_ref().map_err(Clone::clone)?;
75        bound.gate.admit_connect(holder)
76    }
77
78    /// Decides `action` on `channel` for this connection.
79    ///
80    /// # Errors
81    /// As for [`MembershipGate::check`]; a protected channel on a connection
82    /// with no verified subject refuses with that subject's cause.
83    pub fn check(
84        &self,
85        channel: &str,
86        action: MembershipAction,
87    ) -> Result<Decided, MembershipRefusal> {
88        self.decide(channel, action, false)
89    }
90
91    /// [`Self::check`] at a final data-release seam.
92    ///
93    /// # Errors
94    /// As for [`Self::check`].
95    pub fn release(
96        &self,
97        channel: &str,
98        action: MembershipAction,
99    ) -> Result<Decided, MembershipRefusal> {
100        self.decide(channel, action, true)
101    }
102
103    /// [`Self::release`] for a participant conversation, on the channel the
104    /// mapping places it under; an unmapped conversation is not protected.
105    ///
106    /// # Errors
107    /// As for [`Self::check`].
108    pub fn release_conversation(&self, conversation: u64) -> Result<Decided, MembershipRefusal> {
109        let Some(bound) = &self.bound else {
110            return Ok(Decided::NotProtected);
111        };
112        let Some(channel) = bound.gate.channel_for_conversation(conversation) else {
113            return Ok(Decided::NotProtected);
114        };
115        self.decide(channel, MembershipAction::Read, true)
116    }
117
118    pub(super) fn decide(
119        &self,
120        channel: &str,
121        action: MembershipAction,
122        release: bool,
123    ) -> Result<Decided, MembershipRefusal> {
124        let Some(bound) = &self.bound else {
125            return Ok(Decided::NotProtected);
126        };
127        if !bound.gate.protects(channel) {
128            return Ok(Decided::NotProtected);
129        }
130        let holder = match &bound.holder {
131            Ok(holder) => holder,
132            Err(refusal) => {
133                bump(&bound.gate.counters.refused);
134                return Err(refusal.clone());
135            }
136        };
137        if release {
138            bound
139                .gate
140                .check_release(bound.pid, Some(holder), channel, action)
141        } else {
142            bound.gate.check(bound.pid, Some(holder), channel, action)
143        }
144    }
145}
146
147impl ConnectionMembership {
148    /// The final release check a queued delivery owner carries for
149    /// `action` on the protected `channel` (ACCESS-006 R3): `None` when the
150    /// channel is not protected or the connection has no verified subject
151    /// (its admission was already refused, so no such owner exists).
152    #[must_use]
153    pub fn release_check(&self, channel: &str, action: MembershipAction) -> Option<ReleaseCheck> {
154        let bound = self.bound.as_ref()?;
155        if !bound.gate.protects(channel) {
156            return None;
157        }
158        let holder = bound.holder.as_ref().ok()?;
159        Some(ReleaseCheck {
160            gate: Arc::clone(&bound.gate),
161            pid: bound.pid,
162            holder: holder.clone(),
163            channel: channel.to_owned(),
164            action,
165        })
166    }
167}
168
169/// The membership check a queued delivery owner carries to its final release
170/// (ACCESS-006 R3).
171///
172/// It is the same gate, connection, verified subject, channel and action as
173/// its admission, decided again at release against the current generation,
174/// so a deny applied after admission releases nothing.
175#[derive(Clone, Debug)]
176pub struct ReleaseCheck {
177    pub(super) gate: Arc<MembershipGate>,
178    pub(super) pid: u64,
179    pub(super) holder: MembershipSubject,
180    pub(super) channel: String,
181    pub(super) action: MembershipAction,
182}
183
184impl ReleaseCheck {
185    /// Decides the release now.
186    ///
187    /// # Errors
188    /// As for [`MembershipGate::check`].
189    pub fn check(&self) -> Result<Decided, MembershipRefusal> {
190        self.gate
191            .check_release(self.pid, Some(&self.holder), &self.channel, self.action)
192    }
193}
194
195impl Drop for ConnectionMembership {
196    fn drop(&mut self) {
197        if let Some(bound) = &self.bound {
198            bound.gate.release_connection(bound.pid);
199        }
200    }
201}