1use std::{collections::HashSet, fmt::Display, fs::read_to_string, path::PathBuf, str::FromStr};
4
5use nix::unistd::User;
6use serde::{Deserialize, Serialize};
7use signstar_common::{ssh::get_ssh_authorized_key_base_dir, system_user::get_home_base_dir_path};
8use ssh_key::authorized_keys::Entry;
9use uzers::all_users;
10use zeroize::Zeroize;
11
12use crate::{
13 config::Error,
14 state::{StateOrigin, StateOriginInfo},
15 utils::get_current_system_user,
16};
17
18#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Zeroize)]
23#[serde(into = "String", try_from = "String")]
24pub struct SystemUserId(String);
25
26impl SystemUserId {
27 const ROOT: &str = "root";
29
30 pub fn new(user: String) -> Result<Self, crate::Error> {
49 if user.is_empty()
50 || !(user
51 .chars()
52 .all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-'))
53 {
54 return Err(Error::InvalidSystemUserName { name: user }.into());
55 }
56 Ok(Self(user))
57 }
58
59 pub fn root() -> Self {
61 Self(Self::ROOT.to_string())
62 }
63
64 pub fn from_current_unix_user() -> Result<Self, crate::Error> {
74 let current_unix_user = get_current_system_user()?;
75 Self::try_from(current_unix_user)
76 }
77}
78
79impl AsRef<str> for SystemUserId {
80 fn as_ref(&self) -> &str {
81 &self.0
82 }
83}
84
85impl Display for SystemUserId {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 self.0.fmt(f)
88 }
89}
90
91impl From<SystemUserId> for String {
92 fn from(value: SystemUserId) -> Self {
93 value.0
94 }
95}
96
97impl FromStr for SystemUserId {
98 type Err = crate::Error;
99
100 fn from_str(s: &str) -> Result<Self, Self::Err> {
101 Self::new(s.to_string())
102 }
103}
104
105impl TryFrom<String> for SystemUserId {
106 type Error = crate::Error;
107
108 fn try_from(value: String) -> Result<Self, Self::Error> {
109 Self::new(value)
110 }
111}
112
113impl TryFrom<&User> for SystemUserId {
114 type Error = crate::Error;
115
116 fn try_from(value: &User) -> Result<Self, Self::Error> {
117 Self::new(value.name.clone())
118 }
119}
120
121impl TryFrom<User> for SystemUserId {
122 type Error = crate::Error;
123
124 fn try_from(value: User) -> Result<Self, Self::Error> {
125 Self::new(value.name)
126 }
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
135#[serde(into = "String", try_from = "String")]
136pub struct AuthorizedKeyEntry(Entry);
137
138impl AuthorizedKeyEntry {
139 pub fn new(entry: String) -> Result<Self, crate::Error> {
161 Ok(Self(Entry::from_str(&entry).map_err(|_source| {
162 Error::InvalidAuthorizedKeyEntry { entry }
163 })?))
164 }
165}
166
167impl AsRef<Entry> for AuthorizedKeyEntry {
168 fn as_ref(&self) -> &Entry {
169 &self.0
170 }
171}
172
173impl Display for AuthorizedKeyEntry {
174 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175 write!(f, "{}", self.0.to_string())
176 }
177}
178
179impl From<AuthorizedKeyEntry> for String {
180 fn from(value: AuthorizedKeyEntry) -> Self {
181 value.to_string()
182 }
183}
184
185impl FromStr for AuthorizedKeyEntry {
186 type Err = crate::Error;
187
188 fn from_str(s: &str) -> Result<Self, Self::Err> {
189 Self::new(s.to_string())
190 }
191}
192
193impl From<&AuthorizedKeyEntry> for Entry {
194 fn from(value: &AuthorizedKeyEntry) -> Self {
195 value.0.clone()
196 }
197}
198
199impl TryFrom<String> for AuthorizedKeyEntry {
200 type Error = crate::Error;
201
202 fn try_from(value: String) -> Result<Self, crate::Error> {
203 Self::new(value)
204 }
205}
206
207impl std::hash::Hash for AuthorizedKeyEntry {
208 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
209 self.0.to_string().hash(state);
210 }
211}
212
213impl Ord for AuthorizedKeyEntry {
214 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
215 self.0.to_string().cmp(&other.0.to_string())
216 }
217}
218
219impl PartialOrd for AuthorizedKeyEntry {
220 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
221 Some(self.cmp(other))
222 }
223}
224
225#[derive(Clone, Debug, Eq, Hash, PartialEq)]
227pub enum SystemUserData<'a> {
228 BackendAdmin {
234 system_user: SystemUserId,
236 },
237
238 BackendBackup {
240 system_user: &'a SystemUserId,
242 ssh_authorized_key: &'a AuthorizedKeyEntry,
244 },
245
246 BackendCertificateRetrieval {
248 system_user: &'a SystemUserId,
250 ssh_authorized_key: &'a AuthorizedKeyEntry,
252 },
253
254 BackendHermeticMetrics {
260 system_user: &'a SystemUserId,
262 },
263
264 BackendMetrics {
266 system_user: &'a SystemUserId,
268 ssh_authorized_key: &'a AuthorizedKeyEntry,
270 },
271
272 BackendSign {
274 system_user: &'a SystemUserId,
276 ssh_authorized_key: &'a AuthorizedKeyEntry,
278 },
279
280 BackendUpdate {
282 system_user: &'a SystemUserId,
284 ssh_authorized_key: &'a AuthorizedKeyEntry,
286 },
287
288 HostDownloadNetworkConfig {
290 system_user: &'a SystemUserId,
292 ssh_authorized_key: &'a AuthorizedKeyEntry,
294 },
295
296 HostShareholder {
298 system_user: &'a SystemUserId,
300 ssh_authorized_key: &'a AuthorizedKeyEntry,
302 },
303
304 Unknown {
310 system_user: SystemUserId,
312 ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
314 home_dir: PathBuf,
316 },
317}
318
319impl<'a> SystemUserData<'a> {
320 pub fn system_user(&'a self) -> &'a SystemUserId {
322 match self {
323 Self::BackendAdmin { system_user } | Self::Unknown { system_user, .. } => system_user,
324 Self::BackendBackup { system_user, .. }
325 | Self::BackendCertificateRetrieval { system_user, .. }
326 | Self::BackendHermeticMetrics { system_user }
327 | Self::BackendMetrics { system_user, .. }
328 | Self::BackendSign { system_user, .. }
329 | Self::BackendUpdate { system_user, .. }
330 | Self::HostDownloadNetworkConfig { system_user, .. }
331 | Self::HostShareholder { system_user, .. } => system_user,
332 }
333 }
334
335 pub fn ssh_authorized_keys(&'a self) -> Vec<&'a AuthorizedKeyEntry> {
337 match self {
338 Self::BackendAdmin { .. } | Self::BackendHermeticMetrics { .. } => Vec::new(),
339 Self::BackendBackup {
340 ssh_authorized_key, ..
341 }
342 | Self::BackendCertificateRetrieval {
343 ssh_authorized_key, ..
344 }
345 | Self::BackendMetrics {
346 ssh_authorized_key, ..
347 }
348 | Self::BackendSign {
349 ssh_authorized_key, ..
350 }
351 | Self::HostDownloadNetworkConfig {
352 ssh_authorized_key, ..
353 }
354 | Self::HostShareholder {
355 ssh_authorized_key, ..
356 }
357 | Self::BackendUpdate {
358 ssh_authorized_key, ..
359 } => vec![ssh_authorized_key],
360 Self::Unknown {
361 ssh_authorized_keys,
362 ..
363 } => ssh_authorized_keys.iter().collect::<Vec<_>>(),
364 }
365 }
366}
367
368impl<'a> Display for SystemUserData<'a> {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 write!(f, "system user {} ", self.system_user())?;
371 let ssh_authorized_keys = self.ssh_authorized_keys();
372 if let Self::Unknown { home_dir, .. } = self {
373 write!(f, "in home dir {home_dir:?} ")?;
374 }
375 if !ssh_authorized_keys.is_empty() {
376 write!(
377 f,
378 "with ssh keys {} ",
379 ssh_authorized_keys
380 .iter()
381 .map(ToString::to_string)
382 .collect::<Vec<_>>()
383 .join(", ")
384 )?;
385 }
386
387 write!(
388 f,
389 "{}",
390 match self {
391 Self::BackendAdmin { .. } => "for backend administration",
392 Self::BackendBackup { .. } => "for backend backups",
393 Self::BackendCertificateRetrieval { .. } => "for backend certificate retrieval",
394 Self::BackendHermeticMetrics { .. } => "for hermetic backend metrics",
395 Self::BackendMetrics { .. } => "for backend metrics",
396 Self::BackendSign { .. } => "for signing using a backend",
397 Self::BackendUpdate { .. } => "for backend updates",
398 Self::HostDownloadNetworkConfig { .. } => "for downloading host network config",
399 Self::HostShareholder { .. } => "for handling shares of a shared secret",
400 Self::Unknown { .. } => "for unknown use",
401 }
402 )
403 }
404}
405
406#[derive(Debug, Eq, PartialEq)]
408pub struct SystemUserHostState<'a> {
409 pub(crate) system_user_data: HashSet<SystemUserData<'a>>,
410}
411
412impl<'a> SystemUserHostState<'a> {
413 pub const STATE_NAME: &'static str = "system";
415
416 pub fn new() -> Result<Self, crate::Error> {
437 let user_data = unsafe { all_users() }.collect::<Vec<_>>();
438 let mut system_user_data = HashSet::new();
439
440 for user in user_data {
441 let user = User::from_name(&user.name().to_string_lossy())
442 .map_err(|_source| Error::InvalidSystemUserName {
443 name: user.name().to_string_lossy().to_string(),
444 })?
445 .ok_or(Error::InvalidSystemUserName {
446 name: user.name().to_string_lossy().to_string(),
447 })?;
448
449 let ssh_authorized_keys = {
451 let mut ssh_authorized_keys = Vec::new();
452
453 let files = [
454 get_ssh_authorized_key_base_dir()
456 .join(format!("signstar-user-{}.authorized_keys", user.name)),
457 get_home_base_dir_path()
459 .join(&user.name)
460 .join(".ssh")
461 .join("authorized_keys"),
462 ];
463
464 for file in files {
465 if file.is_file() {
466 for line in read_to_string(&file)
467 .map_err(|source| crate::Error::IoPath {
468 path: file,
469 context: "reading the file to string",
470 source,
471 })?
472 .lines()
473 {
474 ssh_authorized_keys.push(AuthorizedKeyEntry::from_str(line)?);
475 }
476 }
477 }
478
479 ssh_authorized_keys
480 };
481
482 system_user_data.insert(SystemUserData::Unknown {
483 system_user: SystemUserId::try_from(&user)?,
484 ssh_authorized_keys,
485 home_dir: user.dir,
486 });
487 }
488
489 Ok(Self { system_user_data })
490 }
491
492 pub fn system_user_data(&self) -> &HashSet<SystemUserData<'a>> {
494 &self.system_user_data
495 }
496}
497
498impl<'a> StateOriginInfo for SystemUserHostState<'a> {
499 fn state_name(&self) -> &str {
500 Self::STATE_NAME
501 }
502
503 fn state_origin(&self) -> StateOrigin {
504 StateOrigin::System
505 }
506}
507
508#[cfg(test)]
509mod tests {
510 use rstest::rstest;
511 use testresult::TestResult;
512
513 use super::*;
514
515 #[test]
516 fn system_user_id_new_fails() {
517 assert!(SystemUserId::new("üser".to_string()).is_err());
518 }
519
520 #[test]
521 fn authorized_key_entry_new_fails() {
522 assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
523 }
524
525 #[test]
526 fn authorized_key_to_string() -> TestResult {
527 let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
528 let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
529
530 assert_eq!(authorized_key.to_string(), entry);
531 Ok(())
532 }
533
534 #[rstest]
535 #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, SystemUserId::new("root".to_string())?)]
536 #[case::backend_backup(SystemUserData::BackendBackup { system_user: &SystemUserId::new("backup".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("backup".to_string())?)]
537 #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, SystemUserId::new("hermetic-metrics".to_string())?)]
538 #[case::backend_metrics(SystemUserData::BackendMetrics { system_user: &SystemUserId::new("metrics".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("metrics".to_string())?)]
539 #[case::backend_sign(SystemUserData::BackendSign { system_user: &SystemUserId::new("sign".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("sign".to_string())?)]
540 #[case::backend_update(SystemUserData::BackendUpdate { system_user: &SystemUserId::new("update".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("update".to_string())?)]
541 #[case::host_download_network_config(SystemUserData::HostDownloadNetworkConfig { system_user: &SystemUserId::new("network-download".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("network-download".to_string())?)]
542 #[case::host_shareholder(SystemUserData::HostShareholder { system_user: &SystemUserId::new("shareholder".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, SystemUserId::new("shareholder".to_string())?)]
543 #[case::unknown(SystemUserData::Unknown { system_user: SystemUserId::new("someone".to_string())?, ssh_authorized_keys: vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?], home_dir: PathBuf::from("/home/someone") }, SystemUserId::new("someone".to_string())?)]
544 fn system_user_data_system_user<'a>(
545 #[case] system_user_data: SystemUserData<'a>,
546 #[case] system_user: SystemUserId,
547 ) -> TestResult {
548 assert_eq!(system_user_data.system_user(), &system_user);
549 Ok(())
550 }
551
552 #[rstest]
553 #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, Vec::new())]
554 #[case::backend_backup(SystemUserData::BackendBackup { system_user: &SystemUserId::new("backup".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
555 #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, Vec::new())]
556 #[case::backend_metrics(SystemUserData::BackendMetrics { system_user: &SystemUserId::new("metrics".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
557 #[case::backend_sign(SystemUserData::BackendSign { system_user: &SystemUserId::new("sign".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
558 #[case::backend_update(SystemUserData::BackendUpdate { system_user: &SystemUserId::new("update".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
559 #[case::host_download_network_config(SystemUserData::HostDownloadNetworkConfig { system_user: &SystemUserId::new("network-download".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
560 #[case::host_shareholder(SystemUserData::HostShareholder { system_user: &SystemUserId::new("shareholder".to_string())?, ssh_authorized_key: &"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()? }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
561 #[case::unknown(SystemUserData::Unknown { system_user: SystemUserId::new("someone".to_string())?, ssh_authorized_keys: vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?], home_dir: PathBuf::from("/home/someone") }, vec!["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host".parse()?])]
562 fn system_user_data_ssh_authorized_keys<'a>(
563 #[case] system_user_data: SystemUserData<'a>,
564 #[case] ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
565 ) -> TestResult {
566 assert_eq!(
567 system_user_data.ssh_authorized_keys(),
568 ssh_authorized_keys.iter().collect::<Vec<_>>()
569 );
570 Ok(())
571 }
572
573 #[cfg(target_os = "linux")]
574 #[test]
575 fn system_user_host_state_new_contains_root() -> TestResult {
576 let state = SystemUserHostState::new()?;
577
578 assert!(state.system_user_data.contains(&SystemUserData::Unknown {
579 system_user: SystemUserId::root(),
580 ssh_authorized_keys: Vec::new(),
581 home_dir: PathBuf::from("/root"),
582 }));
583
584 Ok(())
585 }
586}