1use std::collections::BTreeSet;
4
5use crate::catalog::policy::{Config, ResolvedPolicy, SubjectName};
6use crate::peer::PeerInfo;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct AuthenticatedActor {
11 pub subject: SubjectName,
13 pub authenticated_by: Vec<ProofSummary>,
15 pub presenter: PresenterInfo,
17 pub transport: TransportInfo,
19}
20
21impl AuthenticatedActor {
22 #[must_use]
24 pub const fn unix_uid(&self) -> Option<u32> {
25 self.presenter.uid
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct ProofSummary {
32 pub kind: ProofKind,
34 pub subject: SubjectName,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ProofKind {
41 UnixPeerCredentials,
43 Unauthenticated,
45 SignatureKey,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PresenterInfo {
52 pub pid: Option<u32>,
54 pub uid: Option<u32>,
56 pub gid: Option<u32>,
58 pub executable_path: Option<String>,
60 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#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct TransportInfo {
79 pub kind: TransportKind,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum TransportKind {
86 UnixSocket,
88}
89
90impl Default for TransportInfo {
91 fn default() -> Self {
92 Self {
93 kind: TransportKind::UnixSocket,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum SubjectResolutionError {
101 MissingPeerCredentials,
103 NoSubject {
105 uid: u32,
107 },
108 AmbiguousSubject {
110 uid: u32,
112 subjects: Vec<SubjectName>,
114 },
115 InvalidUnauthenticatedSubject {
117 subject: SubjectName,
119 },
120}
121
122pub 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
134pub 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
160pub 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}