1use crate::ProcessId;
24use std::{borrow::Cow, fmt};
25
26#[derive(Clone, Debug)]
35#[non_exhaustive]
36pub struct Credentials {
37 pub pid: Option<ProcessId>,
39 pub same_user: bool,
41 pub user_evidence: UserEvidence,
43}
44
45impl PartialEq for Credentials {
46 fn eq(&self, other: &Self) -> bool {
47 self.pid == other.pid && self.same_user == other.same_user
48 }
49}
50
51impl Eq for Credentials {}
52
53impl Credentials {
54 pub const fn new(pid: Option<ProcessId>, same_user: bool) -> Self {
58 Self {
59 pid,
60 same_user,
61 user_evidence: UserEvidence::Connection,
62 }
63 }
64
65 pub const fn with_user_evidence(self, evidence: UserEvidence) -> Self {
67 Self {
68 user_evidence: evidence,
69 ..self
70 }
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79#[non_exhaustive]
80pub enum UserEvidence {
81 Connection,
84 PidLookup,
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
94#[non_exhaustive]
95pub enum Rejected {
96 OtherUser,
98 Unreported,
101 NoExpectedProcess,
104 OtherProcess {
106 expected: ProcessId,
108 observed: ProcessId,
110 },
111 NoPolicy,
113 Custom(Cow<'static, str>),
115}
116
117impl fmt::Display for Rejected {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 match self {
120 Self::OtherUser => f.write_str("the peer runs as another OS user"),
121 Self::Unreported => f.write_str("the connection's peer PID is not available"),
122 Self::NoExpectedProcess => f.write_str("no expected process to compare with"),
123 Self::OtherProcess { expected, observed } => {
124 write!(f, "peer PID {observed} is not the expected PID {expected}")
125 }
126 Self::NoPolicy => f.write_str("no policy accepts any peer"),
127 Self::Custom(reason) => f.write_str(reason),
128 }
129 }
130}
131
132impl std::error::Error for Rejected {}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct PidMatch {
138 pid: ProcessId,
139}
140
141impl PidMatch {
142 pub const fn pid(&self) -> ProcessId {
144 self.pid
145 }
146}
147
148fn match_pid(observed: Option<ProcessId>, expected: ProcessId) -> Result<PidMatch, Rejected> {
151 match observed {
152 None => Err(Rejected::Unreported),
153 Some(observed) if observed == expected => Ok(PidMatch { pid: observed }),
154 Some(observed) => Err(Rejected::OtherProcess { expected, observed }),
155 }
156}
157
158pub trait Policy {
160 type Grant;
162 fn grant(&self, credentials: &Credentials) -> Result<Self::Grant, Rejected>;
165}
166
167impl<P: Policy + ?Sized> Policy for &P {
168 type Grant = P::Grant;
169 fn grant(&self, credentials: &Credentials) -> Result<P::Grant, Rejected> {
170 (**self).grant(credentials)
171 }
172}
173
174impl<P: Policy + ?Sized> Policy for Box<P> {
175 type Grant = P::Grant;
176 fn grant(&self, credentials: &Credentials) -> Result<P::Grant, Rejected> {
177 (**self).grant(credentials)
178 }
179}
180
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
183pub struct SameUser;
184
185impl Policy for SameUser {
186 type Grant = ();
187 fn grant(&self, credentials: &Credentials) -> Result<(), Rejected> {
188 if credentials.same_user {
189 Ok(())
190 } else {
191 Err(Rejected::OtherUser)
192 }
193 }
194}
195
196#[derive(Clone, Copy)]
203pub struct ExpectedProcess<F> {
204 expected: F,
205}
206
207impl<F: Fn() -> Option<ProcessId>> ExpectedProcess<F> {
208 pub const fn new(expected: F) -> Self {
210 Self { expected }
211 }
212}
213
214impl<F: Fn() -> Option<ProcessId>> Policy for ExpectedProcess<F> {
215 type Grant = PidMatch;
216 fn grant(&self, credentials: &Credentials) -> Result<PidMatch, Rejected> {
217 SameUser.grant(credentials)?;
218 let expected = (self.expected)().ok_or(Rejected::NoExpectedProcess)?;
219 match_pid(credentials.pid, expected)
220 }
221}
222
223type Tier<G> = Box<dyn Fn(&Credentials) -> Result<G, Rejected> + Send + Sync>;
224
225pub struct First<G> {
248 tiers: Vec<Tier<G>>,
249}
250
251impl<G> Default for First<G> {
252 fn default() -> Self {
253 Self { tiers: Vec::new() }
254 }
255}
256
257impl<G> fmt::Debug for First<G> {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 f.debug_struct("First")
260 .field("tiers", &self.tiers.len())
261 .finish()
262 }
263}
264
265impl<G: Clone + Send + Sync + 'static> First<G> {
266 pub fn new() -> Self {
268 Self::default()
269 }
270
271 pub fn then<P>(mut self, policy: P, grant: G) -> Self
273 where
274 P: Policy + Send + Sync + 'static,
275 {
276 self.tiers.push(Box::new(move |credentials| {
277 policy.grant(credentials).map(|_| grant.clone())
278 }));
279 self
280 }
281}
282
283impl<G> Policy for First<G> {
284 type Grant = G;
285 fn grant(&self, credentials: &Credentials) -> Result<G, Rejected> {
286 let mut rejected = Rejected::NoPolicy;
287 for tier in &self.tiers {
288 match tier(credentials) {
289 Ok(grant) => return Ok(grant),
290 Err(reason) => rejected = reason,
291 }
292 }
293 Err(rejected)
294 }
295}
296
297#[derive(Debug)]
303pub struct Authenticated<C, G> {
304 connection: C,
305 grant: G,
306 credentials: Credentials,
307}
308
309impl<C, G> Authenticated<C, G> {
310 pub fn grant(&self) -> &G {
312 &self.grant
313 }
314
315 pub fn credentials(&self) -> &Credentials {
317 &self.credentials
318 }
319
320 pub fn connection(&self) -> &C {
322 &self.connection
323 }
324
325 pub fn into_parts(self) -> (C, G, Credentials) {
327 (self.connection, self.grant, self.credentials)
328 }
329}
330
331impl<C: std::io::Read, G> std::io::Read for Authenticated<C, G> {
332 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
333 self.connection.read(buf)
334 }
335}
336
337impl<C: std::io::Write, G> std::io::Write for Authenticated<C, G> {
338 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
339 self.connection.write(buf)
340 }
341 fn flush(&mut self) -> std::io::Result<()> {
342 self.connection.flush()
343 }
344}
345
346#[cfg(feature = "async")]
347impl<C: tokio::io::AsyncRead + Unpin, G: Unpin> tokio::io::AsyncRead for Authenticated<C, G> {
348 fn poll_read(
349 self: std::pin::Pin<&mut Self>,
350 cx: &mut std::task::Context<'_>,
351 buf: &mut tokio::io::ReadBuf<'_>,
352 ) -> std::task::Poll<std::io::Result<()>> {
353 std::pin::Pin::new(&mut self.get_mut().connection).poll_read(cx, buf)
354 }
355}
356
357#[cfg(feature = "async")]
358impl<C: tokio::io::AsyncWrite + Unpin, G: Unpin> tokio::io::AsyncWrite for Authenticated<C, G> {
359 fn poll_write(
360 self: std::pin::Pin<&mut Self>,
361 cx: &mut std::task::Context<'_>,
362 buf: &[u8],
363 ) -> std::task::Poll<std::io::Result<usize>> {
364 std::pin::Pin::new(&mut self.get_mut().connection).poll_write(cx, buf)
365 }
366 fn poll_flush(
367 self: std::pin::Pin<&mut Self>,
368 cx: &mut std::task::Context<'_>,
369 ) -> std::task::Poll<std::io::Result<()>> {
370 std::pin::Pin::new(&mut self.get_mut().connection).poll_flush(cx)
371 }
372 fn poll_shutdown(
373 self: std::pin::Pin<&mut Self>,
374 cx: &mut std::task::Context<'_>,
375 ) -> std::task::Poll<std::io::Result<()>> {
376 std::pin::Pin::new(&mut self.get_mut().connection).poll_shutdown(cx)
377 }
378}
379
380pub fn authenticate<C, P: Policy + ?Sized>(
384 connection: C,
385 credentials: Credentials,
386 policy: &P,
387) -> Result<Authenticated<C, P::Grant>, Rejected> {
388 let grant = policy.grant(&credentials)?;
389 Ok(Authenticated {
390 connection,
391 grant,
392 credentials,
393 })
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn pid(value: u32) -> ProcessId {
401 ProcessId::new(value).unwrap()
402 }
403
404 fn creds(pid_value: u32, same_user: bool) -> Credentials {
405 Credentials::new(Some(pid(pid_value)), same_user)
406 }
407
408 #[test]
409 fn credentials_say_where_the_user_check_came_from() {
410 let connection = creds(7, true);
411 assert_eq!(connection.user_evidence, UserEvidence::Connection);
412 let lookup = connection
413 .clone()
414 .with_user_evidence(UserEvidence::PidLookup);
415 assert_eq!(lookup.user_evidence, UserEvidence::PidLookup);
416 assert_eq!(
417 (lookup.pid, lookup.same_user),
418 (connection.pid, connection.same_user)
419 );
420 assert_eq!(lookup, connection);
422 assert_ne!(creds(7, false), connection);
423 assert_ne!(creds(8, true), connection);
424 }
425
426 #[test]
427 fn only_an_equal_reported_pid_matches() {
428 assert_eq!(match_pid(Some(pid(7)), pid(7)).map(|m| m.pid()), Ok(pid(7)));
429 assert_eq!(
430 match_pid(Some(pid(8)), pid(7)),
431 Err(Rejected::OtherProcess {
432 expected: pid(7),
433 observed: pid(8)
434 })
435 );
436 assert_eq!(match_pid(None, pid(7)), Err(Rejected::Unreported));
437 }
438
439 #[test]
440 fn same_user_rejects_another_user_whatever_its_pid() {
441 assert_eq!(SameUser.grant(&creds(7, true)), Ok(()));
442 assert_eq!(SameUser.grant(&Credentials::new(None, true)), Ok(()));
443 assert_eq!(SameUser.grant(&creds(7, false)), Err(Rejected::OtherUser));
444 }
445
446 #[test]
447 fn expected_process_needs_the_same_user_a_record_and_the_same_pid() {
448 let app = ExpectedProcess::new(|| ProcessId::new(42));
449 assert_eq!(app.grant(&creds(42, true)).map(|m| m.pid()), Ok(pid(42)));
450 assert_eq!(app.grant(&creds(42, false)), Err(Rejected::OtherUser));
451 assert_eq!(
452 app.grant(&creds(7, true)),
453 Err(Rejected::OtherProcess {
454 expected: pid(42),
455 observed: pid(7)
456 })
457 );
458 assert_eq!(
459 app.grant(&Credentials::new(None, true)),
460 Err(Rejected::Unreported)
461 );
462 let missing = ExpectedProcess::new(|| None);
463 assert_eq!(
464 missing.grant(&creds(42, true)),
465 Err(Rejected::NoExpectedProcess)
466 );
467 }
468
469 #[derive(Clone, Debug, PartialEq)]
470 enum View {
471 Full,
472 Redacted,
473 }
474
475 fn tiered() -> First<View> {
476 First::new()
477 .then(ExpectedProcess::new(|| ProcessId::new(42)), View::Full)
478 .then(SameUser, View::Redacted)
479 }
480
481 #[test]
482 fn first_grants_the_first_accepting_tier_in_order() {
483 assert_eq!(tiered().grant(&creds(42, true)), Ok(View::Full));
484 assert_eq!(tiered().grant(&creds(7, true)), Ok(View::Redacted));
485 assert_eq!(tiered().grant(&creds(42, false)), Err(Rejected::OtherUser));
486 let shadowed = First::new()
488 .then(SameUser, View::Redacted)
489 .then(ExpectedProcess::new(|| ProcessId::new(42)), View::Full);
490 assert_eq!(shadowed.grant(&creds(42, true)), Ok(View::Redacted));
491 }
492
493 #[test]
494 fn an_empty_list_rejects_every_connection() {
495 assert_eq!(
496 First::<View>::new().grant(&creds(42, true)),
497 Err(Rejected::NoPolicy)
498 );
499 }
500
501 #[test]
502 fn references_and_boxes_of_a_policy_are_policies() {
503 fn by_value<P: Policy>(policy: P, credentials: &Credentials) -> Result<P::Grant, Rejected> {
504 policy.grant(credentials)
505 }
506 let shared = tiered();
507 assert_eq!(by_value(&shared, &creds(42, true)), Ok(View::Full));
508 assert_eq!(by_value(&shared, &creds(7, true)), Ok(View::Redacted));
509 let boxed: Box<dyn Policy<Grant = ()>> = Box::new(SameUser);
510 assert_eq!(boxed.grant(&creds(7, false)), Err(Rejected::OtherUser));
511 assert_eq!(boxed.grant(&creds(7, true)), Ok(()));
512 }
513
514 #[test]
515 fn authenticate_carries_the_grant_and_credentials_with_the_connection() {
516 let credentials = creds(42, true);
517 let accepted = authenticate("conn", credentials.clone(), &tiered()).unwrap();
518 assert_eq!(accepted.grant(), &View::Full);
519 assert_eq!(accepted.credentials(), &credentials);
520 assert_eq!(*accepted.connection(), "conn");
521 assert_eq!(
522 accepted.into_parts(),
523 ("conn", View::Full, credentials.clone())
524 );
525 let dynamic: &dyn Policy<Grant = ()> = &SameUser;
526 assert!(authenticate((), credentials, dynamic).is_ok());
527 assert_eq!(
528 authenticate("conn", creds(1, false), &tiered()).unwrap_err(),
529 Rejected::OtherUser
530 );
531 }
532
533 #[test]
534 fn reads_and_writes_pass_through_to_the_connection() {
535 use std::io::{Read, Write};
536 let mut reader = authenticate(&b"ping"[..], creds(1, true), &SameUser).unwrap();
537 let mut text = String::new();
538 reader.read_to_string(&mut text).unwrap();
539 assert_eq!(text, "ping");
540 let mut writer = authenticate(
543 std::io::BufWriter::new(Vec::new()),
544 creds(1, true),
545 &SameUser,
546 )
547 .unwrap();
548 writer.write_all(b"pong").unwrap();
549 assert!(writer.connection().get_ref().is_empty());
550 writer.flush().unwrap();
551 assert_eq!(writer.connection().get_ref(), b"pong");
552 }
553
554 #[cfg(feature = "async")]
555 #[tokio::test]
556 async fn async_reads_and_writes_pass_through_to_the_connection() {
557 use tokio::io::{AsyncReadExt, AsyncWriteExt};
558 let mut reader = authenticate(&b"ping"[..], creds(1, true), &SameUser).unwrap();
559 let mut text = String::new();
560 reader.read_to_string(&mut text).await.unwrap();
561 assert_eq!(text, "ping");
562 let mut writer = authenticate(
564 tokio::io::BufWriter::new(Vec::new()),
565 creds(1, true),
566 &SameUser,
567 )
568 .unwrap();
569 writer.write_all(b"pong").await.unwrap();
570 assert!(writer.connection().get_ref().is_empty());
571 writer.flush().await.unwrap();
572 assert_eq!(writer.connection().get_ref(), b"pong");
573 writer.shutdown().await.unwrap();
574 }
575
576 #[test]
577 fn first_reports_the_last_tiers_reason_when_none_accepts() {
578 struct Refuse;
579 impl Policy for Refuse {
580 type Grant = ();
581 fn grant(&self, _: &Credentials) -> Result<(), Rejected> {
582 Err(Rejected::Custom("closed for maintenance".into()))
583 }
584 }
585 let refuse_last = First::new()
586 .then(ExpectedProcess::new(|| None), View::Full)
587 .then(Refuse, View::Redacted);
588 assert_eq!(
589 refuse_last.grant(&creds(42, true)),
590 Err(Rejected::Custom("closed for maintenance".into()))
591 );
592 let missing_last = First::new()
593 .then(Refuse, View::Redacted)
594 .then(ExpectedProcess::new(|| None), View::Full);
595 assert_eq!(
596 missing_last.grant(&creds(42, true)),
597 Err(Rejected::NoExpectedProcess)
598 );
599 }
600
601 #[test]
602 fn the_expected_pid_is_read_once_and_only_when_needed() {
603 use std::sync::{
604 Arc,
605 atomic::{AtomicUsize, Ordering},
606 };
607 let calls = Arc::new(AtomicUsize::new(0));
608 let counted = {
609 let calls = Arc::clone(&calls);
610 move || {
611 calls.fetch_add(1, Ordering::SeqCst);
612 ProcessId::new(42)
613 }
614 };
615 let app = ExpectedProcess::new(counted.clone());
616 assert!(app.grant(&creds(42, true)).is_ok());
617 assert_eq!(calls.load(Ordering::SeqCst), 1);
618 assert!(app.grant(&creds(42, false)).is_err());
620 assert_eq!(calls.load(Ordering::SeqCst), 1);
621 let stop_early = First::new()
623 .then(SameUser, View::Redacted)
624 .then(ExpectedProcess::new(counted), View::Full);
625 assert_eq!(stop_early.grant(&creds(42, true)), Ok(View::Redacted));
626 assert_eq!(calls.load(Ordering::SeqCst), 1);
627 }
628
629 #[test]
630 fn every_rejection_says_why() {
631 let cases = [
632 (Rejected::OtherUser, "the peer runs as another OS user"),
633 (
634 Rejected::Unreported,
635 "the connection's peer PID is not available",
636 ),
637 (
638 Rejected::NoExpectedProcess,
639 "no expected process to compare with",
640 ),
641 (
642 Rejected::OtherProcess {
643 expected: pid(42),
644 observed: pid(7),
645 },
646 "peer PID 7 is not the expected PID 42",
647 ),
648 (Rejected::NoPolicy, "no policy accepts any peer"),
649 (Rejected::Custom("closed".into()), "closed"),
650 ];
651 for (rejected, text) in cases {
652 assert_eq!(rejected.to_string(), text);
653 }
654 }
655}