lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

peer.rs (8861B)


      1 //! Platform-bounded local-admin peer authorization.
      2 
      3 use core::fmt;
      4 use std::error::Error;
      5 
      6 /// Host support level for local-admin peer authorization.
      7 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      8 pub enum AdminPeerAuthorizationSupport {
      9     /// Linux requires kernel-reported `SO_PEERCRED` credentials.
     10     LinuxSoPeerCredRequired,
     11     /// macOS v1 relies only on owner-restricted filesystem permissions.
     12     MacOsFilesystemOwnerPermissionsOnly,
     13     /// The local-admin transport is not supported on this platform in v1.
     14     Unsupported,
     15 }
     16 
     17 impl AdminPeerAuthorizationSupport {
     18     /// Returns the exact v1 authorization support for the compilation target.
     19     #[must_use]
     20     pub const fn current() -> Self {
     21         #[cfg(target_os = "linux")]
     22         {
     23             Self::LinuxSoPeerCredRequired
     24         }
     25         #[cfg(target_os = "macos")]
     26         {
     27             Self::MacOsFilesystemOwnerPermissionsOnly
     28         }
     29         #[cfg(not(any(target_os = "linux", target_os = "macos")))]
     30         {
     31             Self::Unsupported
     32         }
     33     }
     34 }
     35 
     36 /// A safe configuration failure for local-admin peer authorization.
     37 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     38 pub enum AdminPeerAuthorizationPolicyError {
     39     InvalidAdminGroupId,
     40     AdminGroupUnsupported,
     41 }
     42 
     43 impl fmt::Display for AdminPeerAuthorizationPolicyError {
     44     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     45         formatter.write_str("admin peer authorization policy is invalid")
     46     }
     47 }
     48 
     49 impl Error for AdminPeerAuthorizationPolicyError {}
     50 
     51 /// One immutable policy shared by Unix-socket permissions and peer admission.
     52 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     53 pub struct AdminPeerAuthorizationPolicy {
     54     admin_gid: Option<u32>,
     55 }
     56 
     57 impl AdminPeerAuthorizationPolicy {
     58     /// Restricts access to the daemon's effective user identity.
     59     #[must_use]
     60     pub const fn owner_only() -> Self {
     61         Self { admin_gid: None }
     62     }
     63 
     64     /// Allows the configured Linux admin group in addition to the daemon user.
     65     pub fn with_admin_gid(admin_gid: u32) -> Result<Self, AdminPeerAuthorizationPolicyError> {
     66         if AdminPeerAuthorizationSupport::current()
     67             != AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired
     68         {
     69             return Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported);
     70         }
     71         if admin_gid == u32::MAX {
     72             return Err(AdminPeerAuthorizationPolicyError::InvalidAdminGroupId);
     73         }
     74         Ok(Self {
     75             admin_gid: Some(admin_gid),
     76         })
     77     }
     78 
     79     /// Returns the host support contract represented by this policy.
     80     #[must_use]
     81     pub const fn support(self) -> AdminPeerAuthorizationSupport {
     82         AdminPeerAuthorizationSupport::current()
     83     }
     84 
     85     /// Returns the configured Linux admin group, when present.
     86     #[must_use]
     87     pub const fn admin_gid(self) -> Option<u32> {
     88         self.admin_gid
     89     }
     90 }
     91 
     92 impl Default for AdminPeerAuthorizationPolicy {
     93     fn default() -> Self {
     94         Self::owner_only()
     95     }
     96 }
     97 
     98 #[cfg(target_os = "linux")]
     99 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    100 pub(crate) enum PeerAuthorizationFailure {
    101     CredentialsUnavailable { kind: std::io::ErrorKind },
    102     Denied,
    103 }
    104 
    105 #[cfg(target_os = "macos")]
    106 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    107 pub(crate) enum PeerAuthorizationFailure {}
    108 
    109 #[cfg(any(target_os = "linux", target_os = "macos"))]
    110 #[derive(Clone, Copy, Debug)]
    111 pub(crate) struct PeerAuthorizer {
    112     policy: AdminPeerAuthorizationPolicy,
    113     daemon_euid: u32,
    114 }
    115 
    116 #[cfg(any(target_os = "linux", target_os = "macos"))]
    117 impl PeerAuthorizer {
    118     pub(crate) const fn new(policy: AdminPeerAuthorizationPolicy, daemon_euid: u32) -> Self {
    119         Self {
    120             policy,
    121             daemon_euid,
    122         }
    123     }
    124 
    125     #[cfg(target_os = "linux")]
    126     pub(crate) fn authorize(
    127         self,
    128         stream: &tokio::net::UnixStream,
    129     ) -> Result<(), PeerAuthorizationFailure> {
    130         self.authorize_linux_result(
    131             stream
    132                 .peer_cred()
    133                 .map(|credentials| (credentials.uid(), credentials.gid()))
    134                 .map_err(|error| error.kind()),
    135         )
    136     }
    137 
    138     #[cfg(target_os = "linux")]
    139     fn authorize_linux_result(
    140         self,
    141         credentials: Result<(u32, u32), std::io::ErrorKind>,
    142     ) -> Result<(), PeerAuthorizationFailure> {
    143         let (peer_uid, peer_gid) = credentials
    144             .map_err(|kind| PeerAuthorizationFailure::CredentialsUnavailable { kind })?;
    145         self.authorize_linux_credentials(peer_uid, peer_gid)
    146     }
    147 
    148     #[cfg(target_os = "linux")]
    149     fn authorize_linux_credentials(
    150         self,
    151         peer_uid: u32,
    152         peer_gid: u32,
    153     ) -> Result<(), PeerAuthorizationFailure> {
    154         if peer_uid == self.daemon_euid || self.policy.admin_gid == Some(peer_gid) {
    155             Ok(())
    156         } else {
    157             Err(PeerAuthorizationFailure::Denied)
    158         }
    159     }
    160 
    161     #[cfg(target_os = "macos")]
    162     pub(crate) const fn authorize(
    163         self,
    164         _stream: &tokio::net::UnixStream,
    165     ) -> Result<(), PeerAuthorizationFailure> {
    166         let _ = self.policy;
    167         let _ = self.daemon_euid;
    168         Ok(())
    169     }
    170 }
    171 
    172 #[cfg(test)]
    173 mod tests {
    174     use super::*;
    175 
    176     #[test]
    177     fn support_inventory_is_exact_for_the_compilation_target() {
    178         #[cfg(target_os = "linux")]
    179         assert_eq!(
    180             AdminPeerAuthorizationSupport::current(),
    181             AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired
    182         );
    183         #[cfg(target_os = "macos")]
    184         assert_eq!(
    185             AdminPeerAuthorizationSupport::current(),
    186             AdminPeerAuthorizationSupport::MacOsFilesystemOwnerPermissionsOnly
    187         );
    188         #[cfg(not(any(target_os = "linux", target_os = "macos")))]
    189         assert_eq!(
    190             AdminPeerAuthorizationSupport::current(),
    191             AdminPeerAuthorizationSupport::Unsupported
    192         );
    193     }
    194 
    195     #[test]
    196     fn owner_policy_is_stable_and_group_policy_is_platform_bounded() {
    197         let owner = AdminPeerAuthorizationPolicy::owner_only();
    198         assert_eq!(AdminPeerAuthorizationPolicy::default(), owner);
    199         assert_eq!(owner.admin_gid(), None);
    200         assert_eq!(owner.support(), AdminPeerAuthorizationSupport::current());
    201         assert_eq!(
    202             AdminPeerAuthorizationPolicy::with_admin_gid(u32::MAX),
    203             Err(if cfg!(target_os = "linux") {
    204                 AdminPeerAuthorizationPolicyError::InvalidAdminGroupId
    205             } else {
    206                 AdminPeerAuthorizationPolicyError::AdminGroupUnsupported
    207             })
    208         );
    209         let error = AdminPeerAuthorizationPolicyError::InvalidAdminGroupId;
    210         assert!(!error.to_string().is_empty());
    211         assert!(error.source().is_none());
    212         #[cfg(target_os = "linux")]
    213         assert_eq!(
    214             AdminPeerAuthorizationPolicy::with_admin_gid(42)
    215                 .expect("valid Linux admin group")
    216                 .admin_gid(),
    217             Some(42)
    218         );
    219         #[cfg(not(target_os = "linux"))]
    220         assert_eq!(
    221             AdminPeerAuthorizationPolicy::with_admin_gid(42),
    222             Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported)
    223         );
    224     }
    225 
    226     #[cfg(target_os = "linux")]
    227     #[tokio::test]
    228     async fn linux_process_credentials_allow_uid_or_gid_and_deny_otherwise() {
    229         let (peer, observed) = tokio::net::UnixStream::pair().expect("Unix stream pair");
    230         let credentials = peer.peer_cred().expect("Linux SO_PEERCRED");
    231         let different_uid = different_id(credentials.uid());
    232         let different_gid = different_id(credentials.gid());
    233 
    234         PeerAuthorizer::new(
    235             AdminPeerAuthorizationPolicy::owner_only(),
    236             credentials.uid(),
    237         )
    238         .authorize(&observed)
    239         .expect("matching daemon euid");
    240         PeerAuthorizer::new(
    241             AdminPeerAuthorizationPolicy::with_admin_gid(credentials.gid())
    242                 .expect("peer group policy"),
    243             different_uid,
    244         )
    245         .authorize(&observed)
    246         .expect("matching configured admin gid");
    247         assert_eq!(
    248             PeerAuthorizer::new(
    249                 AdminPeerAuthorizationPolicy::with_admin_gid(different_gid)
    250                     .expect("different group policy"),
    251                 different_uid,
    252             )
    253             .authorize(&observed),
    254             Err(PeerAuthorizationFailure::Denied)
    255         );
    256         assert_eq!(
    257             PeerAuthorizer::new(
    258                 AdminPeerAuthorizationPolicy::owner_only(),
    259                 credentials.uid(),
    260             )
    261             .authorize_linux_result(Err(std::io::ErrorKind::PermissionDenied)),
    262             Err(PeerAuthorizationFailure::CredentialsUnavailable {
    263                 kind: std::io::ErrorKind::PermissionDenied,
    264             })
    265         );
    266     }
    267 
    268     #[cfg(target_os = "linux")]
    269     fn different_id(id: u32) -> u32 {
    270         if id == 0 { 1 } else { 0 }
    271     }
    272 }