1use std::collections::BTreeSet;
8
9use crate::catalog::policy::{Config, ResolvedPolicy, SubjectName};
10use crate::peer::PeerInfo;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct AuthenticatedActor {
15 pub subject: SubjectName,
17 pub authenticated_by: Vec<ProofSummary>,
19 pub presenter: PresenterInfo,
21 pub transport: TransportInfo,
23}
24
25impl AuthenticatedActor {
26 #[must_use]
28 pub const fn unix_uid(&self) -> Option<u32> {
29 self.presenter.uid
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ProofSummary {
36 pub kind: ProofKind,
38 pub subject: SubjectName,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum ProofKind {
45 UnixPeerCredentials,
47 Unauthenticated,
49 SignatureKey,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct PresenterInfo {
56 pub pid: Option<u32>,
58 pub uid: Option<u32>,
60 pub gid: Option<u32>,
62 pub executable_path: Option<String>,
64 pub display_label: Option<String>,
66}
67
68impl From<&PeerInfo> for PresenterInfo {
69 fn from(peer: &PeerInfo) -> Self {
70 Self {
71 pid: peer.pid,
72 uid: peer.uid,
73 gid: peer.gid,
74 executable_path: peer.executable_path.clone(),
75 display_label: peer.display_label.clone(),
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct TransportInfo {
83 pub kind: TransportKind,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TransportKind {
90 UnixSocket,
92}
93
94impl Default for TransportInfo {
95 fn default() -> Self {
96 Self {
97 kind: TransportKind::UnixSocket,
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum SubjectResolutionError {
105 MissingPeerCredentials,
107 NoSubject {
109 uid: u32,
111 },
112 AmbiguousSubject {
114 uid: u32,
116 subjects: Vec<SubjectName>,
118 },
119 InvalidUnauthenticatedSubject {
121 subject: SubjectName,
123 },
124}
125
126pub fn resolve_local_actor(
128 policy: &ResolvedPolicy,
129 config: &Config,
130 peer: &PeerInfo,
131) -> Result<AuthenticatedActor, SubjectResolutionError> {
132 if let Some(uid) = peer.uid {
133 return resolve_unix_actor(policy, config, peer, uid);
134 }
135 resolve_unauthenticated_actor(policy, peer)
136}
137
138pub fn resolve_unix_actor(
140 policy: &ResolvedPolicy,
141 config: &Config,
142 peer: &PeerInfo,
143 uid: u32,
144) -> Result<AuthenticatedActor, SubjectResolutionError> {
145 let gids: Vec<u32> = config
146 .groups_of(uid)
147 .map(|s| s.iter().copied().collect())
148 .unwrap_or_default();
149 let subjects: Vec<SubjectName> = matching_unix_subjects(policy, uid, &gids)
150 .into_iter()
151 .cloned()
152 .collect();
153 match subjects.as_slice() {
154 [] => Err(SubjectResolutionError::NoSubject { uid }),
155 [subject] => {
156 let mut actor = actor(subject.clone(), ProofKind::UnixPeerCredentials, peer);
157 actor.presenter.display_label = Some(config.user_name_num(uid));
158 Ok(actor)
159 }
160 _ => Err(SubjectResolutionError::AmbiguousSubject { uid, subjects }),
161 }
162}
163
164pub fn resolve_unauthenticated_actor(
166 policy: &ResolvedPolicy,
167 peer: &PeerInfo,
168) -> Result<AuthenticatedActor, SubjectResolutionError> {
169 let Some(subject) = policy.unauthenticated_subject.as_ref() else {
170 return Err(SubjectResolutionError::MissingPeerCredentials);
171 };
172 if !policy
173 .subjects
174 .get(subject)
175 .is_some_and(|definition| definition.match_.matches_unauthenticated())
176 {
177 return Err(SubjectResolutionError::InvalidUnauthenticatedSubject {
178 subject: subject.clone(),
179 });
180 }
181 Ok(actor(subject.clone(), ProofKind::Unauthenticated, peer))
182}
183
184fn matching_unix_subjects<'a>(
185 policy: &'a ResolvedPolicy,
186 uid: u32,
187 gids: &[u32],
188) -> BTreeSet<&'a SubjectName> {
189 policy
190 .subjects
191 .iter()
192 .filter_map(|(name, subject)| subject.match_.matches_unix(uid, gids).then_some(name))
193 .collect()
194}
195
196fn actor(subject: SubjectName, kind: ProofKind, peer: &PeerInfo) -> AuthenticatedActor {
197 AuthenticatedActor {
198 authenticated_by: vec![ProofSummary {
199 kind,
200 subject: subject.clone(),
201 }],
202 subject,
203 presenter: PresenterInfo::from(peer),
204 transport: TransportInfo::default(),
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use std::collections::{BTreeMap, BTreeSet};
211
212 use super::*;
213 use crate::catalog::policy::{PrincipalSpec, ResolvedPolicy, SubjectDefinition, SubjectMatch};
214
215 fn unix_subject(uid: Option<u32>, gid: Option<u32>) -> SubjectDefinition {
216 SubjectDefinition {
217 break_glass: false,
218 match_: SubjectMatch::AllOf(vec![PrincipalSpec::Unix { uid, gid }]),
219 }
220 }
221
222 fn unauthenticated_subject() -> SubjectDefinition {
223 SubjectDefinition {
224 break_glass: false,
225 match_: SubjectMatch::AnyOf(vec![PrincipalSpec::Unauthenticated]),
226 }
227 }
228
229 fn peer(uid: Option<u32>, gid: Option<u32>) -> PeerInfo {
230 PeerInfo {
231 uid,
232 gid,
233 ..PeerInfo::default()
234 }
235 }
236
237 #[test]
238 fn local_unix_actor_resolves_exactly_one_subject() {
239 let policy = ResolvedPolicy {
240 subjects: BTreeMap::from([("svc.web".to_string(), unix_subject(Some(9001), None))]),
241 unauthenticated_subject: None,
242 rules: Vec::new(),
243 };
244 let actor =
245 resolve_local_actor(&policy, &Config::default(), &peer(Some(9001), Some(1))).unwrap();
246 assert_eq!(actor.subject, "svc.web");
247 assert_eq!(actor.unix_uid(), Some(9001));
248 assert_eq!(
249 actor.authenticated_by[0].kind,
250 ProofKind::UnixPeerCredentials
251 );
252 }
253
254 #[test]
255 fn group_subject_uses_configured_memberships_not_primary_gid() {
256 let policy = ResolvedPolicy {
257 subjects: BTreeMap::from([("ops.wheel".to_string(), unix_subject(None, Some(10)))]),
258 unauthenticated_subject: None,
259 rules: Vec::new(),
260 };
261 let mut config = Config::default();
262 config.memberships.insert(5000, BTreeSet::from([5000, 10]));
263 let actor = resolve_local_actor(&policy, &config, &peer(Some(5000), Some(5000))).unwrap();
264 assert_eq!(actor.subject, "ops.wheel");
265
266 let no_membership =
267 resolve_local_actor(&policy, &Config::default(), &peer(Some(5000), Some(10)));
268 assert!(matches!(
269 no_membership,
270 Err(SubjectResolutionError::NoSubject { uid: 5000 })
271 ));
272 }
273
274 #[test]
275 fn ambiguous_unix_actor_is_rejected() {
276 let policy = ResolvedPolicy {
277 subjects: BTreeMap::from([
278 ("svc.one".to_string(), unix_subject(Some(42), None)),
279 ("svc.two".to_string(), unix_subject(Some(42), None)),
280 ]),
281 unauthenticated_subject: None,
282 rules: Vec::new(),
283 };
284 let err = resolve_local_actor(&policy, &Config::default(), &peer(Some(42), None))
285 .expect_err("two subjects must be ambiguous");
286 assert!(matches!(
287 err,
288 SubjectResolutionError::AmbiguousSubject { uid: 42, .. }
289 ));
290 }
291
292 #[test]
293 fn unauthenticated_subject_resolves_only_when_configured() {
294 let policy = ResolvedPolicy {
295 subjects: BTreeMap::from([("guest".to_string(), unauthenticated_subject())]),
296 unauthenticated_subject: Some("guest".to_string()),
297 rules: Vec::new(),
298 };
299 let actor = resolve_local_actor(&policy, &Config::default(), &PeerInfo::default()).unwrap();
300 assert_eq!(actor.subject, "guest");
301 assert_eq!(actor.authenticated_by[0].kind, ProofKind::Unauthenticated);
302
303 let disabled = ResolvedPolicy {
304 unauthenticated_subject: None,
305 ..policy
306 };
307 assert!(matches!(
308 resolve_local_actor(&disabled, &Config::default(), &PeerInfo::default()),
309 Err(SubjectResolutionError::MissingPeerCredentials)
310 ));
311 }
312}