lib

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

commit 40505188fa5ed2e48e5bcdd64d785679456e4ea1
parent 085cc3258f4635f45ef3c16cda74952bf1608448
Author: triesap <tyson@radroots.org>
Date:   Tue, 11 Aug 2026 04:27:05 +0000

service-host: authorize admin peers

- require Linux SO_PEERCRED admission before request dispatch
- bind configured admin groups to exact directory and socket modes
- keep macOS filesystem-only and other platforms explicitly unsupported
- cover process credentials, policy denial, modes, and target compilation

Diffstat:
Mcrates/service_host/src/admin/mod.rs | 9+++++++--
Acrates/service_host/src/admin/peer.rs | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/service_host/src/admin/server.rs | 5+++++
Mcrates/service_host/src/admin/unix.rs | 194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/service_host/src/lib.rs | 6++++--
Mcrates/service_host/tests/package_boundary.rs | 1+
6 files changed, 463 insertions(+), 20 deletions(-)

diff --git a/crates/service_host/src/admin/mod.rs b/crates/service_host/src/admin/mod.rs @@ -4,6 +4,7 @@ mod client; mod limits; mod model; +mod peer; #[cfg(any(target_os = "linux", target_os = "macos"))] mod server; #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -25,6 +26,9 @@ pub use model::{ AdminIdentifierField, AdminMutationRequest, AdminOperationId, AdminPayloadError, AdminSuccessResponse, }; +pub use peer::{ + AdminPeerAuthorizationPolicy, AdminPeerAuthorizationPolicyError, AdminPeerAuthorizationSupport, +}; #[cfg(any(target_os = "linux", target_os = "macos"))] pub use server::{ ADMIN_MIN_RESPONSE_BODY_UTF8_BYTES, ADMIN_ROUTE_PARAMETER_NAME_MAX_UTF8_BYTES, @@ -36,6 +40,7 @@ pub use server::{ }; #[cfg(any(target_os = "linux", target_os = "macos"))] pub use unix::{ - UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE, - UnixAdminSocketBinding, UnixAdminSocketError, UnixAdminSocketWriterAuthority, + UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, UNIX_ADMIN_GROUP_DIRECTORY_MODE, UNIX_ADMIN_GROUP_SOCKET_MODE, + UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding, + UnixAdminSocketError, UnixAdminSocketWriterAuthority, }; diff --git a/crates/service_host/src/admin/peer.rs b/crates/service_host/src/admin/peer.rs @@ -0,0 +1,268 @@ +//! Platform-bounded local-admin peer authorization. + +use core::fmt; +use std::error::Error; + +/// Host support level for local-admin peer authorization. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AdminPeerAuthorizationSupport { + /// Linux requires kernel-reported `SO_PEERCRED` credentials. + LinuxSoPeerCredRequired, + /// macOS v1 relies only on owner-restricted filesystem permissions. + MacOsFilesystemOwnerPermissionsOnly, + /// The local-admin transport is not supported on this platform in v1. + Unsupported, +} + +impl AdminPeerAuthorizationSupport { + /// Returns the exact v1 authorization support for the compilation target. + #[must_use] + pub const fn current() -> Self { + #[cfg(target_os = "linux")] + { + Self::LinuxSoPeerCredRequired + } + #[cfg(target_os = "macos")] + { + Self::MacOsFilesystemOwnerPermissionsOnly + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + Self::Unsupported + } + } +} + +/// A safe configuration failure for local-admin peer authorization. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AdminPeerAuthorizationPolicyError { + InvalidAdminGroupId, + AdminGroupUnsupported, +} + +impl fmt::Display for AdminPeerAuthorizationPolicyError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("admin peer authorization policy is invalid") + } +} + +impl Error for AdminPeerAuthorizationPolicyError {} + +/// One immutable policy shared by Unix-socket permissions and peer admission. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AdminPeerAuthorizationPolicy { + admin_gid: Option<u32>, +} + +impl AdminPeerAuthorizationPolicy { + /// Restricts access to the daemon's effective user identity. + #[must_use] + pub const fn owner_only() -> Self { + Self { admin_gid: None } + } + + /// Allows the configured Linux admin group in addition to the daemon user. + pub fn with_admin_gid(admin_gid: u32) -> Result<Self, AdminPeerAuthorizationPolicyError> { + if AdminPeerAuthorizationSupport::current() + != AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired + { + return Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported); + } + if admin_gid == u32::MAX { + return Err(AdminPeerAuthorizationPolicyError::InvalidAdminGroupId); + } + Ok(Self { + admin_gid: Some(admin_gid), + }) + } + + /// Returns the host support contract represented by this policy. + #[must_use] + pub const fn support(self) -> AdminPeerAuthorizationSupport { + AdminPeerAuthorizationSupport::current() + } + + /// Returns the configured Linux admin group, when present. + #[must_use] + pub const fn admin_gid(self) -> Option<u32> { + self.admin_gid + } +} + +impl Default for AdminPeerAuthorizationPolicy { + fn default() -> Self { + Self::owner_only() + } +} + +#[cfg(target_os = "linux")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PeerAuthorizationFailure { + CredentialsUnavailable { kind: std::io::ErrorKind }, + Denied, +} + +#[cfg(target_os = "macos")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum PeerAuthorizationFailure {} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[derive(Clone, Copy, Debug)] +pub(crate) struct PeerAuthorizer { + policy: AdminPeerAuthorizationPolicy, + daemon_euid: u32, +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl PeerAuthorizer { + pub(crate) const fn new(policy: AdminPeerAuthorizationPolicy, daemon_euid: u32) -> Self { + Self { + policy, + daemon_euid, + } + } + + #[cfg(target_os = "linux")] + pub(crate) fn authorize( + self, + stream: &tokio::net::UnixStream, + ) -> Result<(), PeerAuthorizationFailure> { + self.authorize_linux_result( + stream + .peer_cred() + .map(|credentials| (credentials.uid(), credentials.gid())) + .map_err(|error| error.kind()), + ) + } + + #[cfg(target_os = "linux")] + fn authorize_linux_result( + self, + credentials: Result<(u32, u32), std::io::ErrorKind>, + ) -> Result<(), PeerAuthorizationFailure> { + let (peer_uid, peer_gid) = credentials + .map_err(|kind| PeerAuthorizationFailure::CredentialsUnavailable { kind })?; + self.authorize_linux_credentials(peer_uid, peer_gid) + } + + #[cfg(target_os = "linux")] + fn authorize_linux_credentials( + self, + peer_uid: u32, + peer_gid: u32, + ) -> Result<(), PeerAuthorizationFailure> { + if peer_uid == self.daemon_euid || self.policy.admin_gid == Some(peer_gid) { + Ok(()) + } else { + Err(PeerAuthorizationFailure::Denied) + } + } + + #[cfg(target_os = "macos")] + pub(crate) const fn authorize( + self, + _stream: &tokio::net::UnixStream, + ) -> Result<(), PeerAuthorizationFailure> { + let _ = self.policy; + let _ = self.daemon_euid; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn support_inventory_is_exact_for_the_compilation_target() { + #[cfg(target_os = "linux")] + assert_eq!( + AdminPeerAuthorizationSupport::current(), + AdminPeerAuthorizationSupport::LinuxSoPeerCredRequired + ); + #[cfg(target_os = "macos")] + assert_eq!( + AdminPeerAuthorizationSupport::current(), + AdminPeerAuthorizationSupport::MacOsFilesystemOwnerPermissionsOnly + ); + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + assert_eq!( + AdminPeerAuthorizationSupport::current(), + AdminPeerAuthorizationSupport::Unsupported + ); + } + + #[test] + fn owner_policy_is_stable_and_group_policy_is_platform_bounded() { + let owner = AdminPeerAuthorizationPolicy::owner_only(); + assert_eq!(owner.admin_gid(), None); + assert_eq!(owner.support(), AdminPeerAuthorizationSupport::current()); + assert_eq!( + AdminPeerAuthorizationPolicy::with_admin_gid(u32::MAX), + Err(if cfg!(target_os = "linux") { + AdminPeerAuthorizationPolicyError::InvalidAdminGroupId + } else { + AdminPeerAuthorizationPolicyError::AdminGroupUnsupported + }) + ); + #[cfg(target_os = "linux")] + assert_eq!( + AdminPeerAuthorizationPolicy::with_admin_gid(42) + .expect("valid Linux admin group") + .admin_gid(), + Some(42) + ); + #[cfg(not(target_os = "linux"))] + assert_eq!( + AdminPeerAuthorizationPolicy::with_admin_gid(42), + Err(AdminPeerAuthorizationPolicyError::AdminGroupUnsupported) + ); + } + + #[cfg(target_os = "linux")] + #[tokio::test] + async fn linux_process_credentials_allow_uid_or_gid_and_deny_otherwise() { + let (peer, observed) = tokio::net::UnixStream::pair().expect("Unix stream pair"); + let credentials = peer.peer_cred().expect("Linux SO_PEERCRED"); + let different_uid = different_id(credentials.uid()); + let different_gid = different_id(credentials.gid()); + + PeerAuthorizer::new( + AdminPeerAuthorizationPolicy::owner_only(), + credentials.uid(), + ) + .authorize(&observed) + .expect("matching daemon euid"); + PeerAuthorizer::new( + AdminPeerAuthorizationPolicy::with_admin_gid(credentials.gid()) + .expect("peer group policy"), + different_uid, + ) + .authorize(&observed) + .expect("matching configured admin gid"); + assert_eq!( + PeerAuthorizer::new( + AdminPeerAuthorizationPolicy::with_admin_gid(different_gid) + .expect("different group policy"), + different_uid, + ) + .authorize(&observed), + Err(PeerAuthorizationFailure::Denied) + ); + assert_eq!( + PeerAuthorizer::new( + AdminPeerAuthorizationPolicy::owner_only(), + credentials.uid(), + ) + .authorize_linux_result(Err(std::io::ErrorKind::PermissionDenied)), + Err(PeerAuthorizationFailure::CredentialsUnavailable { + kind: std::io::ErrorKind::PermissionDenied, + }) + ); + } + + #[cfg(target_os = "linux")] + fn different_id(id: u32) -> u32 { + if id == 0 { 1 } else { 0 } + } +} diff --git a/crates/service_host/src/admin/server.rs b/crates/service_host/src/admin/server.rs @@ -754,6 +754,7 @@ impl AdminServer { .map_err(|error| AdminServerError::ListenerRegistration { kind: error.kind() })?; let listener = tokio::net::UnixListener::from_std(listener) .map_err(|error| AdminServerError::ListenerRegistration { kind: error.kind() })?; + let peer_authorizer = binding.peer_authorizer(); let permits = Arc::new(Semaphore::new( self.state.limits.concurrent_connections() as usize )); @@ -777,6 +778,10 @@ impl AdminServer { break Err(AdminServerError::Accept { kind: error.kind() }); } }; + if peer_authorizer.authorize(&stream).is_err() { + drop(stream); + continue; + } let Ok(permit) = Arc::clone(&permits).try_acquire_owned() else { drop(stream); continue; diff --git a/crates/service_host/src/admin/unix.rs b/crates/service_host/src/admin/unix.rs @@ -11,14 +11,22 @@ use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf}; use std::time::Duration; -use rustix::fs::{FileType, Mode, OFlags, fchmod, fstat, open, openat}; +use rustix::fs::{ + AtFlags, FileType, Gid, Mode, OFlags, chownat, fchmod, fchown, fstat, open, openat, +}; use rustix::process::geteuid; +use super::peer::{AdminPeerAuthorizationPolicy, PeerAuthorizer}; + const WRITER_LOCK_FILE_NAME: &str = ".radroots-admin-writer.lock"; /// Final owner-only mode for the runtime directory. pub const UNIX_ADMIN_OWNER_DIRECTORY_MODE: u32 = 0o700; /// Final owner-only mode for the socket path. pub const UNIX_ADMIN_OWNER_SOCKET_MODE: u32 = 0o600; +/// Final Linux mode for a runtime directory shared with an admin group. +pub const UNIX_ADMIN_GROUP_DIRECTORY_MODE: u32 = 0o750; +/// Final Linux mode for a socket shared with an admin group. +pub const UNIX_ADMIN_GROUP_SOCKET_MODE: u32 = 0o660; /// Maximum time spent proving that an existing Unix socket has a live listener. pub const UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT: Duration = Duration::from_secs(1); @@ -32,6 +40,7 @@ pub enum UnixAdminSocketError { RuntimeDirectoryWrongOwner, RuntimeDirectoryChanged, RuntimeDirectoryPermissions { kind: io::ErrorKind }, + RuntimeDirectoryGroup { kind: io::ErrorKind }, InvalidSocketPath, WriterLockUnavailable { kind: io::ErrorKind }, WriterLockInvalidType, @@ -45,6 +54,7 @@ pub enum UnixAdminSocketError { StaleSocketCleanup { kind: io::ErrorKind }, SocketBind { kind: io::ErrorKind }, SocketPermissions { kind: io::ErrorKind }, + SocketGroup { kind: io::ErrorKind }, ListenerConfiguration { kind: io::ErrorKind }, } @@ -59,6 +69,9 @@ impl fmt::Display for UnixAdminSocketError { Self::RuntimeDirectoryPermissions { .. } => { "admin runtime directory permissions could not be secured" } + Self::RuntimeDirectoryGroup { .. } => { + "admin runtime directory group could not be secured" + } Self::InvalidSocketPath => "admin socket path is outside its runtime directory", Self::WriterLockUnavailable { .. } => "admin writer lock is unavailable", Self::WriterLockInvalidType => "admin writer lock path has an unsafe type", @@ -72,6 +85,7 @@ impl fmt::Display for UnixAdminSocketError { Self::StaleSocketCleanup { .. } => "stale admin socket could not be removed", Self::SocketBind { .. } => "admin socket could not be bound", Self::SocketPermissions { .. } => "admin socket permissions could not be secured", + Self::SocketGroup { .. } => "admin socket group could not be secured", Self::ListenerConfiguration { .. } => "admin listener could not be configured", }) } @@ -103,6 +117,9 @@ pub struct UnixAdminSocketWriterAuthority { _directory: File, directory_identity: FileIdentity, expected_uid: u32, + expected_gid: Option<u32>, + directory_mode: u32, + peer_authorization: AdminPeerAuthorizationPolicy, _writer_lock: File, } @@ -119,20 +136,23 @@ impl fmt::Debug for UnixAdminSocketWriterAuthority { impl UnixAdminSocketWriterAuthority { /// Acquires owner-only writer authority for one existing runtime directory. pub fn acquire(runtime_directory: impl AsRef<Path>) -> Result<Self, UnixAdminSocketError> { + Self::acquire_with_peer_authorization( + runtime_directory, + AdminPeerAuthorizationPolicy::owner_only(), + ) + } + + /// Acquires writer authority using one policy for permissions and peer admission. + pub fn acquire_with_peer_authorization( + runtime_directory: impl AsRef<Path>, + peer_authorization: AdminPeerAuthorizationPolicy, + ) -> Result<Self, UnixAdminSocketError> { let runtime_directory = runtime_directory.as_ref().to_path_buf(); if !runtime_directory.is_absolute() { return Err(UnixAdminSocketError::RuntimeDirectoryNotAbsolute); } let expected_uid = geteuid().as_raw(); let directory = open_secure_directory(&runtime_directory, expected_uid)?; - fchmod(&directory, Mode::RWXU).map_err(|error| { - UnixAdminSocketError::RuntimeDirectoryPermissions { - kind: errno_kind(error), - } - })?; - let directory_metadata = directory.metadata().map_err(|error| { - UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() } - })?; let writer_lock = open_writer_lock(&directory, expected_uid)?; FileExt::try_lock_exclusive(&writer_lock).map_err(|error| { if error.kind() == io::ErrorKind::WouldBlock { @@ -141,12 +161,37 @@ impl UnixAdminSocketWriterAuthority { UnixAdminSocketError::WriterLockUnavailable { kind: error.kind() } } })?; + let expected_gid = peer_authorization.admin_gid(); + if let Some(admin_gid) = expected_gid { + fchown(&directory, None, Some(Gid::from_raw(admin_gid))).map_err(|error| { + UnixAdminSocketError::RuntimeDirectoryGroup { + kind: errno_kind(error), + } + })?; + } + let directory_permissions = if expected_gid.is_some() { + Mode::RWXU | Mode::RGRP | Mode::XGRP + } else { + Mode::RWXU + }; + let directory_mode: u32 = directory_permissions.bits().into(); + fchmod(&directory, directory_permissions).map_err(|error| { + UnixAdminSocketError::RuntimeDirectoryPermissions { + kind: errno_kind(error), + } + })?; + let directory_metadata = directory.metadata().map_err(|error| { + UnixAdminSocketError::RuntimeDirectoryUnavailable { kind: error.kind() } + })?; let authority = Self { runtime_directory, _directory: directory, directory_identity: FileIdentity::from_metadata(&directory_metadata), expected_uid, + expected_gid, + directory_mode, + peer_authorization, _writer_lock: writer_lock, }; authority.ensure_directory_identity()?; @@ -173,6 +218,8 @@ impl UnixAdminSocketWriterAuthority { return Err(UnixAdminSocketError::RuntimeDirectoryChanged); } if metadata.uid() != self.expected_uid + || self.expected_gid.is_some_and(|gid| metadata.gid() != gid) + || metadata.permissions().mode() & 0o777 != self.directory_mode || FileIdentity::from_metadata(&metadata) != self.directory_identity { return Err(UnixAdminSocketError::RuntimeDirectoryChanged); @@ -181,7 +228,7 @@ impl UnixAdminSocketWriterAuthority { } } -/// A bound owner-only Unix admin listener with identity-safe cleanup. +/// A bound Unix admin listener with policy-aligned modes and identity-safe cleanup. pub struct UnixAdminSocketBinding { listener: UnixListener, socket_path: PathBuf, @@ -225,13 +272,45 @@ impl UnixAdminSocketBinding { return Err(error); } }; - if let Err(error) = fs::set_permissions( + if let Some(admin_gid) = authority.expected_gid + && let Err(error) = chownat( + &authority._directory, + socket_path + .file_name() + .expect("resolved socket path always has a file name"), + None, + Some(Gid::from_raw(admin_gid)), + AtFlags::SYMLINK_NOFOLLOW, + ) + { + drop(listener); + remove_matching_socket(&authority, &socket_path, socket_identity); + return Err(UnixAdminSocketError::SocketGroup { + kind: errno_kind(error), + }); + } + let socket_mode = if authority.expected_gid.is_some() { + UNIX_ADMIN_GROUP_SOCKET_MODE + } else { + UNIX_ADMIN_OWNER_SOCKET_MODE + }; + if let Err(error) = + fs::set_permissions(&socket_path, fs::Permissions::from_mode(socket_mode)) + { + drop(listener); + remove_matching_socket(&authority, &socket_path, socket_identity); + return Err(UnixAdminSocketError::SocketPermissions { kind: error.kind() }); + } + if let Err(error) = verify_bound_socket( &socket_path, - fs::Permissions::from_mode(UNIX_ADMIN_OWNER_SOCKET_MODE), + socket_identity, + authority.expected_uid, + authority.expected_gid, + socket_mode, ) { drop(listener); remove_matching_socket(&authority, &socket_path, socket_identity); - return Err(UnixAdminSocketError::SocketPermissions { kind: error.kind() }); + return Err(error); } if let Err(error) = listener.set_nonblocking(true) { drop(listener); @@ -247,11 +326,22 @@ impl UnixAdminSocketBinding { }) } - /// Borrows the nonblocking listener without transferring cleanup authority. - #[must_use] - pub fn listener(&self) -> &UnixListener { + pub(crate) fn listener(&self) -> &UnixListener { &self.listener } + + /// Returns the policy shared by socket permissions and peer admission. + #[must_use] + pub const fn peer_authorization(&self) -> AdminPeerAuthorizationPolicy { + self.authority.peer_authorization + } + + pub(crate) const fn peer_authorizer(&self) -> PeerAuthorizer { + PeerAuthorizer::new( + self.authority.peer_authorization, + self.authority.expected_uid, + ) + } } impl Drop for UnixAdminSocketBinding { @@ -374,6 +464,36 @@ fn inspect_socket( } } +fn verify_bound_socket( + socket_path: &Path, + expected_identity: FileIdentity, + expected_uid: u32, + expected_gid: Option<u32>, + expected_mode: u32, +) -> Result<(), UnixAdminSocketError> { + let metadata = fs::symlink_metadata(socket_path) + .map_err(|error| UnixAdminSocketError::SocketPathUnavailable { kind: error.kind() })?; + if !metadata.file_type().is_socket() + || FileIdentity::from_metadata(&metadata) != expected_identity + { + return Err(UnixAdminSocketError::SocketPathWrongType); + } + if metadata.uid() != expected_uid { + return Err(UnixAdminSocketError::SocketPathWrongOwner); + } + if expected_gid.is_some_and(|gid| metadata.gid() != gid) { + return Err(UnixAdminSocketError::SocketGroup { + kind: io::ErrorKind::PermissionDenied, + }); + } + if metadata.permissions().mode() & 0o777 != expected_mode { + return Err(UnixAdminSocketError::SocketPermissions { + kind: io::ErrorKind::PermissionDenied, + }); + } + Ok(()) +} + fn remove_matching_socket( authority: &UnixAdminSocketWriterAuthority, socket_path: &Path, @@ -442,9 +562,51 @@ mod tests { fn owner_only_mode_inventory_is_literal_and_stable() { assert_eq!(UNIX_ADMIN_OWNER_DIRECTORY_MODE, 0o700); assert_eq!(UNIX_ADMIN_OWNER_SOCKET_MODE, 0o600); + assert_eq!(UNIX_ADMIN_GROUP_DIRECTORY_MODE, 0o750); + assert_eq!(UNIX_ADMIN_GROUP_SOCKET_MODE, 0o660); assert_eq!(u32::from(Mode::RWXU.bits()), 0o700); } + #[cfg(target_os = "linux")] + #[tokio::test] + async fn configured_admin_group_sets_group_access_modes_and_identity() { + let directory = tempfile::tempdir().expect("temporary directory"); + let socket = directory.path().join("admin.sock"); + let admin_gid = rustix::process::getegid().as_raw(); + let policy = AdminPeerAuthorizationPolicy::with_admin_gid(admin_gid) + .expect("current Linux group is valid"); + let authority = UnixAdminSocketWriterAuthority::acquire_with_peer_authorization( + directory.path(), + policy, + ) + .expect("group writer authority"); + let binding = UnixAdminSocketBinding::bind(authority, &socket) + .await + .expect("group admin binding"); + + assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE); + assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE); + assert_eq!( + fs::symlink_metadata(directory.path()) + .expect("directory metadata") + .gid(), + admin_gid + ); + assert_eq!( + fs::symlink_metadata(&socket) + .expect("socket metadata") + .gid(), + admin_gid + ); + assert_eq!(binding.peer_authorization(), policy); + + let error = UnixAdminSocketWriterAuthority::acquire(directory.path()) + .expect_err("active group-authorized writer excludes owner-only reconfiguration"); + assert_eq!(error, UnixAdminSocketError::WriterAlreadyActive); + assert_eq!(mode(directory.path()), UNIX_ADMIN_GROUP_DIRECTORY_MODE); + assert_eq!(mode(&socket), UNIX_ADMIN_GROUP_SOCKET_MODE); + } + #[tokio::test] async fn refuses_a_live_socket_owned_outside_the_writer_guard() { let directory = tempfile::tempdir().expect("temporary directory"); diff --git a/crates/service_host/src/lib.rs b/crates/service_host/src/lib.rs @@ -16,6 +16,7 @@ pub use admin::{ AdminContractVersionError, AdminCorrelationId, AdminError, AdminErrorCode, AdminErrorCodeError, AdminErrorMessage, AdminErrorMessageError, AdminFailureResponse, AdminIdentifierError, AdminIdentifierField, AdminMutationRequest, AdminOperationId, AdminPayloadError, + AdminPeerAuthorizationPolicy, AdminPeerAuthorizationPolicyError, AdminPeerAuthorizationSupport, AdminSuccessResponse, AdminTransportLimitField, AdminTransportLimitValues, AdminTransportLimits, AdminTransportLimitsError, }; @@ -28,8 +29,9 @@ pub use admin::{ AdminRouteFailureStatus, AdminRouteOutcome, AdminRouteOutcomeError, AdminRoutePath, AdminRoutePathError, AdminRouteRegistrationError, AdminRouter, AdminServer, AdminServerConfigError, AdminServerError, UNIX_ADMIN_ACTIVE_PROBE_TIMEOUT, - UNIX_ADMIN_OWNER_DIRECTORY_MODE, UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding, - UnixAdminSocketError, UnixAdminSocketWriterAuthority, + UNIX_ADMIN_GROUP_DIRECTORY_MODE, UNIX_ADMIN_GROUP_SOCKET_MODE, UNIX_ADMIN_OWNER_DIRECTORY_MODE, + UNIX_ADMIN_OWNER_SOCKET_MODE, UnixAdminSocketBinding, UnixAdminSocketError, + UnixAdminSocketWriterAuthority, }; pub use build_info::{ BuildInfo, BuildInfoEnvironment, BuildInfoError, BuildInfoField, BuildMode, ContractVersions, diff --git a/crates/service_host/tests/package_boundary.rs b/crates/service_host/tests/package_boundary.rs @@ -7,6 +7,7 @@ const ADMIN_SOURCE: &str = concat!( include_str!("../src/admin/client.rs"), include_str!("../src/admin/limits.rs"), include_str!("../src/admin/model.rs"), + include_str!("../src/admin/peer.rs"), include_str!("../src/admin/server.rs"), include_str!("../src/admin/unix.rs"), );