1use std::fs::File;
2use std::path::PathBuf;
3
4use crate::error::{Error, Result};
5use crate::exec::{self, Tools};
6
7#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14pub struct MachineIdentity {
15 pub principal: String,
17 pub realm: String,
19 pub keytab: PathBuf,
20}
21
22impl MachineIdentity {
23 pub fn discover(tools: &Tools, keytab: impl Into<PathBuf>) -> Result<Self> {
29 Self::pick(tools, keytab, None)
30 }
31
32 pub fn discover_in_realm(
36 tools: &Tools,
37 keytab: impl Into<PathBuf>,
38 realm: &str,
39 ) -> Result<Self> {
40 Self::pick(tools, keytab, Some(realm))
41 }
42
43 fn pick(tools: &Tools, keytab: impl Into<PathBuf>, want_realm: Option<&str>) -> Result<Self> {
44 let keytab = keytab.into();
45 if let Err(source) = File::open(&keytab) {
47 return Err(if source.kind() == std::io::ErrorKind::NotFound {
48 Error::KeytabMissing { keytab }
49 } else {
50 Error::KeytabUnreadable { keytab, source }
51 });
52 }
53 let listing = exec::run_ok(
55 &tools.klist,
56 ["-k".as_ref(), "--".as_ref(), keytab.as_os_str()],
57 )?;
58 let principals = parse_machine_principals(&listing);
59 let principal = select_principal(&principals, want_realm)
60 .cloned()
61 .ok_or_else(|| match want_realm {
62 Some(realm) => Error::NoMachinePrincipalForRealm {
63 realm: realm.to_string(),
64 keytab: keytab.clone(),
65 },
66 None => Error::NoMachinePrincipal {
67 keytab: keytab.clone(),
68 },
69 })?;
70 let realm = principal
71 .split_once('@')
72 .map(|(_, r)| r.to_string())
73 .unwrap_or_default();
74 Ok(Self {
75 principal,
76 realm,
77 keytab,
78 })
79 }
80}
81
82pub(crate) fn select_principal<'a>(
85 principals: &'a [String],
86 want_realm: Option<&str>,
87) -> Option<&'a String> {
88 match want_realm {
89 Some(realm) => principals.iter().find(|p| {
90 p.rsplit_once('@')
91 .is_some_and(|(_, r)| r.eq_ignore_ascii_case(realm))
92 }),
93 None => principals.first(),
94 }
95}
96
97pub(crate) fn parse_machine_principals(klist_k: &str) -> Vec<String> {
103 let mut found: Vec<String> = Vec::new();
104 for line in klist_k.lines() {
105 let mut fields = line.split_whitespace();
106 let (Some(kvno), Some(principal)) = (fields.next(), fields.next()) else {
107 continue;
108 };
109 if kvno.is_empty() || !kvno.bytes().all(|b| b.is_ascii_digit()) {
111 continue;
112 }
113 let Some((name, realm)) = principal.split_once('@') else {
114 continue;
115 };
116 if name.ends_with('$')
120 && !name.starts_with('-')
121 && !name.contains('/')
122 && !realm.is_empty()
123 && !found.iter().any(|p| p == principal)
124 {
125 found.push(principal.to_string());
126 }
127 }
128 found
129}
130
131#[cfg(test)]
132mod tests {
133 use super::parse_machine_principals;
134
135 const KLIST_K: &str = "\
136Keytab name: FILE:/etc/krb5.keytab
137KVNO Principal
138---- --------------------------------------------------------------------------
139 2 WORKSTATION1$@EXAMPLE.COM
140 2 WORKSTATION1$@EXAMPLE.COM
141 2 host/WORKSTATION1@EXAMPLE.COM
142 2 host/workstation1.example.com@EXAMPLE.COM
143 2 RestrictedKrbHost/WORKSTATION1@EXAMPLE.COM
144";
145
146 #[test]
147 fn picks_machine_principal_only_once() {
148 let got = parse_machine_principals(KLIST_K);
149 assert_eq!(got, vec!["WORKSTATION1$@EXAMPLE.COM".to_string()]);
150 }
151
152 #[test]
153 fn empty_and_garbage_input() {
154 assert!(parse_machine_principals("").is_empty());
155 assert!(parse_machine_principals("no keytab entries here\n----\n").is_empty());
156 assert!(parse_machine_principals("KVNO X$@R\n").is_empty());
158 }
159
160 #[test]
161 fn skips_service_principals_with_dollar_realm() {
162 let got = parse_machine_principals(" 3 host/weird$@REALM\n 3 GOOD$@REALM\n");
164 assert_eq!(got, vec!["GOOD$@REALM".to_string()]);
165 }
166
167 #[test]
168 fn rejects_leading_dash_principals() {
169 let got = parse_machine_principals(" 2 -x$@REALM\n 2 OK$@REALM\n");
171 assert_eq!(got, vec!["OK$@REALM".to_string()]);
172 }
173
174 #[test]
175 fn selects_principal_by_realm() {
176 let ps = vec!["WS1$@REALM.A".to_string(), "WS1$@REALM.B".to_string()];
177 assert_eq!(super::select_principal(&ps, None), Some(&ps[0]));
179 assert_eq!(super::select_principal(&ps, Some("realm.b")), Some(&ps[1]));
181 assert_eq!(super::select_principal(&ps, Some("REALM.C")), None);
183 assert_eq!(super::select_principal(&[], None), None);
184 }
185}