use std::collections::BTreeSet;
use crate::catalog::policy::{Config, ResolvedPolicy, SubjectName};
use crate::peer::PeerInfo;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthenticatedActor {
pub subject: SubjectName,
pub authenticated_by: Vec<ProofSummary>,
pub presenter: PresenterInfo,
pub transport: TransportInfo,
}
impl AuthenticatedActor {
#[must_use]
pub const fn unix_uid(&self) -> Option<u32> {
self.presenter.uid
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProofSummary {
pub kind: ProofKind,
pub subject: SubjectName,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProofKind {
UnixPeerCredentials,
Unauthenticated,
SignatureKey,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PresenterInfo {
pub pid: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub executable_path: Option<String>,
pub display_label: Option<String>,
}
impl From<&PeerInfo> for PresenterInfo {
fn from(peer: &PeerInfo) -> Self {
Self {
pid: peer.pid,
uid: peer.uid,
gid: peer.gid,
executable_path: peer.executable_path.clone(),
display_label: peer.display_label.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransportInfo {
pub kind: TransportKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportKind {
UnixSocket,
}
impl Default for TransportInfo {
fn default() -> Self {
Self {
kind: TransportKind::UnixSocket,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubjectResolutionError {
MissingPeerCredentials,
NoSubject {
uid: u32,
},
AmbiguousSubject {
uid: u32,
subjects: Vec<SubjectName>,
},
InvalidUnauthenticatedSubject {
subject: SubjectName,
},
}
pub fn resolve_local_actor(
policy: &ResolvedPolicy,
config: &Config,
peer: &PeerInfo,
) -> Result<AuthenticatedActor, SubjectResolutionError> {
if let Some(uid) = peer.uid {
return resolve_unix_actor(policy, config, peer, uid);
}
resolve_unauthenticated_actor(policy, peer)
}
pub fn resolve_unix_actor(
policy: &ResolvedPolicy,
config: &Config,
peer: &PeerInfo,
uid: u32,
) -> Result<AuthenticatedActor, SubjectResolutionError> {
let gids: Vec<u32> = config
.groups_of(uid)
.map(|s| s.iter().copied().collect())
.unwrap_or_default();
let subjects: Vec<SubjectName> = matching_unix_subjects(policy, uid, &gids)
.into_iter()
.cloned()
.collect();
match subjects.as_slice() {
[] => Err(SubjectResolutionError::NoSubject { uid }),
[subject] => {
let mut actor = actor(subject.clone(), ProofKind::UnixPeerCredentials, peer);
actor.presenter.display_label = Some(config.user_name_num(uid));
Ok(actor)
}
_ => Err(SubjectResolutionError::AmbiguousSubject { uid, subjects }),
}
}
pub fn resolve_unauthenticated_actor(
policy: &ResolvedPolicy,
peer: &PeerInfo,
) -> Result<AuthenticatedActor, SubjectResolutionError> {
let Some(subject) = policy.unauthenticated_subject.as_ref() else {
return Err(SubjectResolutionError::MissingPeerCredentials);
};
if !policy
.subjects
.get(subject)
.is_some_and(|definition| definition.match_.matches_unauthenticated())
{
return Err(SubjectResolutionError::InvalidUnauthenticatedSubject {
subject: subject.clone(),
});
}
Ok(actor(subject.clone(), ProofKind::Unauthenticated, peer))
}
fn matching_unix_subjects<'a>(
policy: &'a ResolvedPolicy,
uid: u32,
gids: &[u32],
) -> BTreeSet<&'a SubjectName> {
policy
.subjects
.iter()
.filter_map(|(name, subject)| subject.match_.matches_unix(uid, gids).then_some(name))
.collect()
}
fn actor(subject: SubjectName, kind: ProofKind, peer: &PeerInfo) -> AuthenticatedActor {
AuthenticatedActor {
authenticated_by: vec![ProofSummary {
kind,
subject: subject.clone(),
}],
subject,
presenter: PresenterInfo::from(peer),
transport: TransportInfo::default(),
}
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, BTreeSet};
use super::*;
use crate::catalog::policy::{PrincipalSpec, ResolvedPolicy, SubjectDefinition, SubjectMatch};
fn unix_subject(uid: Option<u32>, gid: Option<u32>) -> SubjectDefinition {
SubjectDefinition {
break_glass: false,
match_: SubjectMatch::AllOf(vec![PrincipalSpec::Unix { uid, gid }]),
}
}
fn unauthenticated_subject() -> SubjectDefinition {
SubjectDefinition {
break_glass: false,
match_: SubjectMatch::AnyOf(vec![PrincipalSpec::Unauthenticated]),
}
}
fn peer(uid: Option<u32>, gid: Option<u32>) -> PeerInfo {
PeerInfo {
uid,
gid,
..PeerInfo::default()
}
}
#[test]
fn local_unix_actor_resolves_exactly_one_subject() {
let policy = ResolvedPolicy {
subjects: BTreeMap::from([("svc.web".to_string(), unix_subject(Some(9001), None))]),
unauthenticated_subject: None,
rules: Vec::new(),
};
let actor =
resolve_local_actor(&policy, &Config::default(), &peer(Some(9001), Some(1))).unwrap();
assert_eq!(actor.subject, "svc.web");
assert_eq!(actor.unix_uid(), Some(9001));
assert_eq!(
actor.authenticated_by[0].kind,
ProofKind::UnixPeerCredentials
);
}
#[test]
fn group_subject_uses_configured_memberships_not_primary_gid() {
let policy = ResolvedPolicy {
subjects: BTreeMap::from([("ops.wheel".to_string(), unix_subject(None, Some(10)))]),
unauthenticated_subject: None,
rules: Vec::new(),
};
let mut config = Config::default();
config.memberships.insert(5000, BTreeSet::from([5000, 10]));
let actor = resolve_local_actor(&policy, &config, &peer(Some(5000), Some(5000))).unwrap();
assert_eq!(actor.subject, "ops.wheel");
let no_membership =
resolve_local_actor(&policy, &Config::default(), &peer(Some(5000), Some(10)));
assert!(matches!(
no_membership,
Err(SubjectResolutionError::NoSubject { uid: 5000 })
));
}
#[test]
fn ambiguous_unix_actor_is_rejected() {
let policy = ResolvedPolicy {
subjects: BTreeMap::from([
("svc.one".to_string(), unix_subject(Some(42), None)),
("svc.two".to_string(), unix_subject(Some(42), None)),
]),
unauthenticated_subject: None,
rules: Vec::new(),
};
let err = resolve_local_actor(&policy, &Config::default(), &peer(Some(42), None))
.expect_err("two subjects must be ambiguous");
assert!(matches!(
err,
SubjectResolutionError::AmbiguousSubject { uid: 42, .. }
));
}
#[test]
fn unauthenticated_subject_resolves_only_when_configured() {
let policy = ResolvedPolicy {
subjects: BTreeMap::from([("guest".to_string(), unauthenticated_subject())]),
unauthenticated_subject: Some("guest".to_string()),
rules: Vec::new(),
};
let actor = resolve_local_actor(&policy, &Config::default(), &PeerInfo::default()).unwrap();
assert_eq!(actor.subject, "guest");
assert_eq!(actor.authenticated_by[0].kind, ProofKind::Unauthenticated);
let disabled = ResolvedPolicy {
unauthenticated_subject: None,
..policy
};
assert!(matches!(
resolve_local_actor(&disabled, &Config::default(), &PeerInfo::default()),
Err(SubjectResolutionError::MissingPeerCredentials)
));
}
}