Skip to main content

moq_auth/
lease.rs

1//! The handle a session holds for the grant that admitted it.
2//!
3//! Whoever runs the accept loop decides how a session is authorized, builds a
4//! [`Producer`], and hands the [`Consumer`] to the session. The session reads the
5//! current [`Grant`], waits for it to change, and learns when it is revoked. The
6//! producer side is driven by [`Client`](crate::Client) when an auth server answers, or
7//! by any in-process logic when the embedder decides itself. No trait, no callbacks.
8
9use std::task::Poll;
10
11use serde::{Deserialize, Serialize};
12
13use crate::{Bytes, Grant};
14
15/// Why a lease ended, from whichever side ended it.
16///
17/// On the wire an `end` event carries it as one string: the fixed spellings below,
18/// or the session's own classification verbatim.
19#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub enum Reason {
22	/// A handle was dropped without saying why.
23	Dropped,
24	/// The grant reached its `expires`.
25	Expired,
26	/// The auth server refused the session on a re-check.
27	Refused,
28	/// The auth server answered a grant the relay cannot honor.
29	Invalid,
30	/// The session ended for its own reason, named by whoever closed it.
31	Session(String),
32}
33
34impl Reason {
35	/// The wire spelling.
36	pub fn as_str(&self) -> &str {
37		match self {
38			Self::Dropped => "dropped",
39			Self::Expired => "expired",
40			Self::Refused => "refused",
41			Self::Invalid => "invalid",
42			Self::Session(reason) => reason,
43		}
44	}
45}
46
47impl std::fmt::Display for Reason {
48	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49		f.write_str(self.as_str())
50	}
51}
52
53impl From<&str> for Reason {
54	fn from(reason: &str) -> Self {
55		match reason {
56			"dropped" => Self::Dropped,
57			"expired" => Self::Expired,
58			"refused" => Self::Refused,
59			"invalid" => Self::Invalid,
60			other => Self::Session(other.to_string()),
61		}
62	}
63}
64
65impl From<String> for Reason {
66	fn from(reason: String) -> Self {
67		Self::from(reason.as_str())
68	}
69}
70
71impl Serialize for Reason {
72	fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
73		serializer.serialize_str(self.as_str())
74	}
75}
76
77impl<'de> Deserialize<'de> for Reason {
78	fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
79		Ok(String::deserialize(deserializer)?.into())
80	}
81}
82
83#[derive(Debug)]
84struct State {
85	grant: Grant,
86	/// Bumped on every update, so a consumer can tell a change from a spurious wake.
87	epoch: u64,
88	/// Set once by whichever side ends the lease first; the other side reads it.
89	closed: Option<(Reason, Bytes)>,
90	/// Bumped on each re-check ask; the producer observes and clears, so a burst
91	/// coalesces into one wake.
92	revalidate: u64,
93}
94
95/// The authorizing side of a lease: applies new grants and revokes.
96///
97/// Dropping it revokes with [`Reason::Dropped`] unless the lease already ended.
98#[derive(Debug)]
99pub struct Producer {
100	state: kio::Shared<State>,
101}
102
103impl Producer {
104	/// Start a lease on `grant`, returning both handles.
105	pub fn new(grant: Grant) -> (Self, Consumer) {
106		let state = kio::Shared::new(State {
107			grant,
108			epoch: 0,
109			closed: None,
110			revalidate: 0,
111		});
112		(Self { state: state.clone() }, Consumer { state, seen: 0 })
113	}
114
115	/// Replace the grant, waking the consumer. A no-op once the lease ended.
116	pub fn update(&self, grant: Grant) {
117		let mut state = self.state.lock();
118		if state.closed.is_some() {
119			return;
120		}
121		state.grant = grant;
122		state.epoch += 1;
123	}
124
125	/// End the lease with `reason`, consuming the handle, and return the reason
126	/// the lease ended with: `reason`, or the consumer's if it closed first.
127	pub fn revoke(self, reason: Reason) -> Reason {
128		self.finish(reason, Bytes::default()).0
129	}
130
131	/// Poll for the lease ending, from either side: why it ended, and the totals
132	/// the session reported. A producer-side revoke, or a drop, reports zero bytes.
133	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<(Reason, Bytes)> {
134		let state = std::task::ready!(self.state.poll(waiter, |state| ready_if(state.closed.is_some())));
135		Poll::Ready(state.closed.clone().expect("waited for a close"))
136	}
137
138	/// Wait for the lease to end, from either side.
139	pub async fn closed(&self) -> (Reason, Bytes) {
140		kio::wait(|waiter| self.poll_closed(waiter)).await
141	}
142
143	/// Poll until at least one re-check has been asked since the last observation.
144	/// A burst of asks resolves once.
145	pub fn poll_revalidate(&self, waiter: &kio::Waiter) -> Poll<()> {
146		let mut state = std::task::ready!(self.state.poll(waiter, |state| ready_if(state.revalidate > 0)));
147		state.revalidate = 0;
148		Poll::Ready(())
149	}
150
151	/// Wait until a re-check has been asked since the last observation.
152	pub async fn revalidate_requested(&self) {
153		kio::wait(|waiter| self.poll_revalidate(waiter)).await
154	}
155
156	pub(crate) fn finish(&self, reason: Reason, bytes: Bytes) -> (Reason, Bytes) {
157		self.state.lock().closed.get_or_insert((reason, bytes)).clone()
158	}
159}
160
161impl Drop for Producer {
162	fn drop(&mut self) {
163		self.finish(Reason::Dropped, Bytes::default());
164	}
165}
166
167/// The session's side of a lease: reads the grant and learns when it changes or ends.
168///
169/// Dropping it ends the lease with [`Reason::Dropped`]; [`close`](Self::close) says why.
170#[derive(Debug)]
171pub struct Consumer {
172	state: kio::Shared<State>,
173	seen: u64,
174}
175
176impl Consumer {
177	/// A lease on a grant nobody drives: it never changes and is never revoked,
178	/// so only the holder ends it. What a static or public grant admits under.
179	pub fn fixed(grant: Grant) -> Self {
180		let state = kio::Shared::new(State {
181			grant,
182			epoch: 0,
183			closed: None,
184			revalidate: 0,
185		});
186		Self { state, seen: 0 }
187	}
188
189	/// The grant as it stands now.
190	pub fn grant(&self) -> Grant {
191		self.state.read().grant.clone()
192	}
193
194	/// Poll for the next update: the new grant, or the reason the lease ended.
195	pub fn poll_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Grant, Reason>> {
196		let seen = self.seen;
197		let state = std::task::ready!(
198			self.state
199				.poll(waiter, |state| ready_if(state.epoch > seen || state.closed.is_some()))
200		);
201		if let Some((reason, _)) = &state.closed {
202			return Poll::Ready(Err(reason.clone()));
203		}
204		self.seen = state.epoch;
205		Poll::Ready(Ok(state.grant.clone()))
206	}
207
208	/// Wait for the next update: the new grant, or the reason the lease ended.
209	pub async fn changed(&mut self) -> Result<Grant, Reason> {
210		kio::wait(|waiter| self.poll_changed(waiter)).await
211	}
212
213	/// Poll for the lease ending.
214	pub fn poll_closed(&self, waiter: &kio::Waiter) -> Poll<Reason> {
215		let state = std::task::ready!(self.state.poll(waiter, |state| ready_if(state.closed.is_some())));
216		Poll::Ready(state.closed.as_ref().expect("waited for a close").0.clone())
217	}
218
219	/// Wait for the lease to end.
220	pub async fn closed(&self) -> Reason {
221		kio::wait(|waiter| self.poll_closed(waiter)).await
222	}
223
224	/// Ask the producer to re-check now. A no-op on a [`fixed`](Self::fixed)
225	/// lease, which has nobody to ask.
226	pub fn revalidate(&self) {
227		self.state.lock().revalidate += 1;
228	}
229
230	/// End the lease with the session's own close classification and the totals it
231	/// moved, consuming the handle, and return the reason the lease ended with:
232	/// `reason`, or the producer's if it revoked first. [`Drop`] reports zero bytes.
233	pub fn close(self, reason: impl Into<Reason>, bytes: Bytes) -> Reason {
234		self.state.lock().closed.get_or_insert((reason.into(), bytes)).0.clone()
235	}
236}
237
238impl Drop for Consumer {
239	fn drop(&mut self) {
240		self.state
241			.lock()
242			.closed
243			.get_or_insert((Reason::Dropped, Bytes::default()));
244	}
245}
246
247fn ready_if(condition: bool) -> Poll<()> {
248	if condition { Poll::Ready(()) } else { Poll::Pending }
249}
250
251#[cfg(test)]
252mod tests {
253	use super::*;
254	use std::future::Future;
255	use std::pin::pin;
256	use std::task::{Context, Waker};
257
258	fn grant(publish: &str) -> Grant {
259		Grant::new([publish.parse().unwrap()].into_iter().collect(), Default::default())
260	}
261
262	fn poll<F: Future>(future: F) -> Poll<F::Output> {
263		pin!(future).poll(&mut Context::from_waker(Waker::noop()))
264	}
265
266	#[test]
267	fn update_wakes_the_consumer_once_per_change() {
268		let (producer, mut consumer) = Producer::new(grant("a/**"));
269		assert_eq!(consumer.grant(), grant("a/**"));
270		assert!(poll(consumer.changed()).is_pending());
271
272		producer.update(grant("b/**"));
273		assert_eq!(poll(consumer.changed()), Poll::Ready(Ok(grant("b/**"))));
274		assert_eq!(consumer.grant(), grant("b/**"));
275		assert!(poll(consumer.changed()).is_pending());
276	}
277
278	#[test]
279	fn revoke_reaches_the_consumer() {
280		let (producer, mut consumer) = Producer::new(grant("a/**"));
281		assert!(poll(consumer.closed()).is_pending());
282
283		producer.revoke(Reason::Refused);
284		assert_eq!(poll(consumer.closed()), Poll::Ready(Reason::Refused));
285		assert_eq!(poll(consumer.changed()), Poll::Ready(Err(Reason::Refused)));
286	}
287
288	#[test]
289	fn the_session_close_hands_over_byte_totals() {
290		let (producer, consumer) = Producer::new(grant("a/**"));
291		consumer.close("done", Bytes { sent: 3, received: 5 });
292		assert_eq!(
293			poll(producer.closed()),
294			Poll::Ready((Reason::Session("done".into()), Bytes { sent: 3, received: 5 }))
295		);
296	}
297
298	#[test]
299	fn dropping_the_producer_revokes() {
300		let (producer, consumer) = Producer::new(grant("a/**"));
301		drop(producer);
302		assert_eq!(poll(consumer.closed()), Poll::Ready(Reason::Dropped));
303	}
304
305	#[test]
306	fn dropping_the_consumer_reports_zero_bytes() {
307		let (producer, consumer) = Producer::new(grant("a/**"));
308		drop(consumer);
309		assert_eq!(
310			poll(producer.closed()),
311			Poll::Ready((Reason::Dropped, Bytes::default()))
312		);
313	}
314
315	#[test]
316	fn the_session_close_reaches_the_producer_and_the_first_reason_wins() {
317		let (producer, consumer) = Producer::new(grant("a/**"));
318		assert!(poll(producer.closed()).is_pending());
319
320		let recorded = consumer.close("disconnected", Bytes { sent: 1, received: 2 });
321		assert_eq!(recorded, Reason::Session("disconnected".into()));
322		assert_eq!(
323			poll(producer.closed()),
324			Poll::Ready((Reason::Session("disconnected".into()), Bytes { sent: 1, received: 2 }))
325		);
326
327		// A later revocation changes nothing, and says so.
328		assert_eq!(producer.revoke(Reason::Expired), Reason::Session("disconnected".into()));
329	}
330
331	#[test]
332	fn a_fixed_lease_only_ends_by_the_holder() {
333		let mut consumer = Consumer::fixed(grant("a/**"));
334		assert_eq!(consumer.grant(), grant("a/**"));
335		assert!(poll(consumer.changed()).is_pending());
336		assert!(poll(consumer.closed()).is_pending());
337		consumer.revalidate();
338		assert_eq!(consumer.grant(), grant("a/**"));
339		assert!(poll(consumer.changed()).is_pending());
340		assert!(poll(consumer.closed()).is_pending());
341		assert_eq!(consumer.close("done", Bytes::default()), Reason::Session("done".into()));
342	}
343
344	#[test]
345	fn n_nudges_wake_the_producer_once() {
346		let (producer, consumer) = Producer::new(grant("a/**"));
347		assert!(poll(producer.revalidate_requested()).is_pending());
348
349		for _ in 0..8 {
350			consumer.revalidate();
351		}
352		assert_eq!(poll(producer.revalidate_requested()), Poll::Ready(()));
353		assert!(poll(producer.revalidate_requested()).is_pending());
354
355		consumer.revalidate();
356		assert_eq!(poll(producer.revalidate_requested()), Poll::Ready(()));
357	}
358
359	#[test]
360	fn reason_round_trips_as_one_string() {
361		for (reason, text) in [
362			(Reason::Dropped, "\"dropped\""),
363			(Reason::Expired, "\"expired\""),
364			(Reason::Refused, "\"refused\""),
365			(Reason::Invalid, "\"invalid\""),
366			(Reason::Session("protocol error".into()), "\"protocol error\""),
367		] {
368			assert_eq!(serde_json::to_string(&reason).unwrap(), text);
369			assert_eq!(serde_json::from_str::<Reason>(text).unwrap(), reason);
370		}
371	}
372}