Skip to main content

basil_core/core/
actor.rs

1//! Authenticated authorization actor resolution.
2
3use std::collections::BTreeSet;
4
5use crate::catalog::policy::{Config, ResolvedPolicy, SubjectName};
6use crate::peer::PeerInfo;
7
8/// A resolved actor that the `PDP` can authorize.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct AuthenticatedActor {
11    /// The subject selected for authorization.
12    pub subject: SubjectName,
13    /// Evidence summaries that established the subject.
14    pub authenticated_by: Vec<ProofSummary>,
15    /// The local presenter that brought the request to the broker.
16    pub presenter: PresenterInfo,
17    /// Transport facts for the request.
18    pub transport: TransportInfo,
19}
20
21impl AuthenticatedActor {
22    /// The presenter's `SO_PEERCRED` uid, when the actor came over a Unix socket.
23    #[must_use]
24    pub const fn unix_uid(&self) -> Option<u32> {
25        self.presenter.uid
26    }
27}
28
29/// Evidence that established an [`AuthenticatedActor`].
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ProofSummary {
32    /// The kind of proof.
33    pub kind: ProofKind,
34    /// The subject established by this proof.
35    pub subject: SubjectName,
36}
37
38/// Supported first-cut proof kinds.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ProofKind {
41    /// Local Unix peer credentials from `SO_PEERCRED`.
42    UnixPeerCredentials,
43    /// Explicit configured anonymous subject.
44    Unauthenticated,
45    /// Signed sealed-invocation envelope with a configured public key.
46    SignatureKey,
47}
48
49/// Information about the process or bridge that presented the request.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PresenterInfo {
52    /// `SO_PEERCRED` process id.
53    pub pid: Option<u32>,
54    /// `SO_PEERCRED` uid.
55    pub uid: Option<u32>,
56    /// `SO_PEERCRED` primary gid.
57    pub gid: Option<u32>,
58    /// Best-effort executable path from `/proc`.
59    pub executable_path: Option<String>,
60    /// Human-readable presenter label.
61    pub display_label: Option<String>,
62}
63
64impl From<&PeerInfo> for PresenterInfo {
65    fn from(peer: &PeerInfo) -> Self {
66        Self {
67            pid: peer.pid,
68            uid: peer.uid,
69            gid: peer.gid,
70            executable_path: peer.executable_path.clone(),
71            display_label: peer.display_label.clone(),
72        }
73    }
74}
75
76/// Transport facts for the request carrying the actor proof.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct TransportInfo {
79    /// The transport class.
80    pub kind: TransportKind,
81}
82
83/// Supported transport classes.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum TransportKind {
86    /// Local Unix-domain socket.
87    UnixSocket,
88}
89
90impl Default for TransportInfo {
91    fn default() -> Self {
92        Self {
93            kind: TransportKind::UnixSocket,
94        }
95    }
96}
97
98/// Why actor resolution failed.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum SubjectResolutionError {
101    /// No Unix credentials were present and no explicit unauthenticated subject applies.
102    MissingPeerCredentials,
103    /// Unix credentials were present but matched no subject.
104    NoSubject {
105        /// The presenter's uid.
106        uid: u32,
107    },
108    /// Unix credentials matched more than one subject and no request-level subject disambiguated it.
109    AmbiguousSubject {
110        /// The presenter's uid.
111        uid: u32,
112        /// Matching subjects.
113        subjects: Vec<SubjectName>,
114    },
115    /// The configured unauthenticated subject is absent or not an unauthenticated subject.
116    InvalidUnauthenticatedSubject {
117        /// The configured subject name.
118        subject: SubjectName,
119    },
120}
121
122/// Resolve a local request actor from captured peer information.
123pub fn resolve_local_actor(
124    policy: &ResolvedPolicy,
125    config: &Config,
126    peer: &PeerInfo,
127) -> Result<AuthenticatedActor, SubjectResolutionError> {
128    if let Some(uid) = peer.uid {
129        return resolve_unix_actor(policy, config, peer, uid);
130    }
131    resolve_unauthenticated_actor(policy, peer)
132}
133
134/// Resolve a Unix actor by uid with optional presenter context.
135pub fn resolve_unix_actor(
136    policy: &ResolvedPolicy,
137    config: &Config,
138    peer: &PeerInfo,
139    uid: u32,
140) -> Result<AuthenticatedActor, SubjectResolutionError> {
141    let gids: Vec<u32> = config
142        .groups_of(uid)
143        .map(|s| s.iter().copied().collect())
144        .unwrap_or_default();
145    let subjects: Vec<SubjectName> = matching_unix_subjects(policy, uid, &gids)
146        .into_iter()
147        .cloned()
148        .collect();
149    match subjects.as_slice() {
150        [] => Err(SubjectResolutionError::NoSubject { uid }),
151        [subject] => {
152            let mut actor = actor(subject.clone(), ProofKind::UnixPeerCredentials, peer);
153            actor.presenter.display_label = Some(config.user_name_num(uid));
154            Ok(actor)
155        }
156        _ => Err(SubjectResolutionError::AmbiguousSubject { uid, subjects }),
157    }
158}
159
160/// Resolve the configured unauthenticated actor, if enabled.
161pub fn resolve_unauthenticated_actor(
162    policy: &ResolvedPolicy,
163    peer: &PeerInfo,
164) -> Result<AuthenticatedActor, SubjectResolutionError> {
165    let Some(subject) = policy.unauthenticated_subject.as_ref() else {
166        return Err(SubjectResolutionError::MissingPeerCredentials);
167    };
168    if !policy
169        .subjects
170        .get(subject)
171        .is_some_and(|definition| definition.match_.matches_unauthenticated())
172    {
173        return Err(SubjectResolutionError::InvalidUnauthenticatedSubject {
174            subject: subject.clone(),
175        });
176    }
177    Ok(actor(subject.clone(), ProofKind::Unauthenticated, peer))
178}
179
180fn matching_unix_subjects<'a>(
181    policy: &'a ResolvedPolicy,
182    uid: u32,
183    gids: &[u32],
184) -> BTreeSet<&'a SubjectName> {
185    policy
186        .subjects
187        .iter()
188        .filter_map(|(name, subject)| subject.match_.matches_unix(uid, gids).then_some(name))
189        .collect()
190}
191
192fn actor(subject: SubjectName, kind: ProofKind, peer: &PeerInfo) -> AuthenticatedActor {
193    AuthenticatedActor {
194        authenticated_by: vec![ProofSummary {
195            kind,
196            subject: subject.clone(),
197        }],
198        subject,
199        presenter: PresenterInfo::from(peer),
200        transport: TransportInfo::default(),
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use std::collections::{BTreeMap, BTreeSet};
207
208    use super::*;
209    use crate::catalog::policy::{PrincipalSpec, ResolvedPolicy, SubjectDefinition, SubjectMatch};
210
211    fn unix_subject(uid: Option<u32>, gid: Option<u32>) -> SubjectDefinition {
212        SubjectDefinition {
213            break_glass: false,
214            match_: SubjectMatch::AllOf(vec![PrincipalSpec::Unix { uid, gid }]),
215        }
216    }
217
218    fn unauthenticated_subject() -> SubjectDefinition {
219        SubjectDefinition {
220            break_glass: false,
221            match_: SubjectMatch::AnyOf(vec![PrincipalSpec::Unauthenticated]),
222        }
223    }
224
225    fn peer(uid: Option<u32>, gid: Option<u32>) -> PeerInfo {
226        PeerInfo {
227            uid,
228            gid,
229            ..PeerInfo::default()
230        }
231    }
232
233    #[test]
234    fn local_unix_actor_resolves_exactly_one_subject() {
235        let policy = ResolvedPolicy {
236            subjects: BTreeMap::from([("svc.web".to_string(), unix_subject(Some(9001), None))]),
237            unauthenticated_subject: None,
238            rules: Vec::new(),
239        };
240        let actor =
241            resolve_local_actor(&policy, &Config::default(), &peer(Some(9001), Some(1))).unwrap();
242        assert_eq!(actor.subject, "svc.web");
243        assert_eq!(actor.unix_uid(), Some(9001));
244        assert_eq!(
245            actor.authenticated_by[0].kind,
246            ProofKind::UnixPeerCredentials
247        );
248    }
249
250    #[test]
251    fn group_subject_uses_configured_memberships_not_primary_gid() {
252        let policy = ResolvedPolicy {
253            subjects: BTreeMap::from([("ops.wheel".to_string(), unix_subject(None, Some(10)))]),
254            unauthenticated_subject: None,
255            rules: Vec::new(),
256        };
257        let mut config = Config::default();
258        config.memberships.insert(5000, BTreeSet::from([5000, 10]));
259        let actor = resolve_local_actor(&policy, &config, &peer(Some(5000), Some(5000))).unwrap();
260        assert_eq!(actor.subject, "ops.wheel");
261
262        let no_membership =
263            resolve_local_actor(&policy, &Config::default(), &peer(Some(5000), Some(10)));
264        assert!(matches!(
265            no_membership,
266            Err(SubjectResolutionError::NoSubject { uid: 5000 })
267        ));
268    }
269
270    #[test]
271    fn ambiguous_unix_actor_is_rejected() {
272        let policy = ResolvedPolicy {
273            subjects: BTreeMap::from([
274                ("svc.one".to_string(), unix_subject(Some(42), None)),
275                ("svc.two".to_string(), unix_subject(Some(42), None)),
276            ]),
277            unauthenticated_subject: None,
278            rules: Vec::new(),
279        };
280        let err = resolve_local_actor(&policy, &Config::default(), &peer(Some(42), None))
281            .expect_err("two subjects must be ambiguous");
282        assert!(matches!(
283            err,
284            SubjectResolutionError::AmbiguousSubject { uid: 42, .. }
285        ));
286    }
287
288    #[test]
289    fn unauthenticated_subject_resolves_only_when_configured() {
290        let policy = ResolvedPolicy {
291            subjects: BTreeMap::from([("guest".to_string(), unauthenticated_subject())]),
292            unauthenticated_subject: Some("guest".to_string()),
293            rules: Vec::new(),
294        };
295        let actor = resolve_local_actor(&policy, &Config::default(), &PeerInfo::default()).unwrap();
296        assert_eq!(actor.subject, "guest");
297        assert_eq!(actor.authenticated_by[0].kind, ProofKind::Unauthenticated);
298
299        let disabled = ResolvedPolicy {
300            unauthenticated_subject: None,
301            ..policy
302        };
303        assert!(matches!(
304            resolve_local_actor(&disabled, &Config::default(), &PeerInfo::default()),
305            Err(SubjectResolutionError::MissingPeerCredentials)
306        ));
307    }
308}