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 BackendHermeticMetrics {
252 system_user: &'a SystemUserId,
254 },
255
256 BackendMetrics {
258 system_user: &'a SystemUserId,
260 ssh_authorized_key: &'a AuthorizedKeyEntry,
262 },
263
264 BackendSign {
266 system_user: &'a SystemUserId,
268 ssh_authorized_key: &'a AuthorizedKeyEntry,
270 },
271
272 BackendUpdate {
274 system_user: &'a SystemUserId,
276 ssh_authorized_key: &'a AuthorizedKeyEntry,
278 },
279
280 HostDownloadNetworkConfig {
282 system_user: &'a SystemUserId,
284 ssh_authorized_key: &'a AuthorizedKeyEntry,
286 },
287
288 HostShareholder {
290 system_user: &'a SystemUserId,
292 ssh_authorized_key: &'a AuthorizedKeyEntry,
294 },
295
296 Unknown {
302 system_user: SystemUserId,
304 ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
306 home_dir: PathBuf,
308 },
309}
310
311impl<'a> SystemUserData<'a> {
312 pub fn system_user(&'a self) -> &'a SystemUserId {
314 match self {
315 Self::BackendAdmin { system_user } | Self::Unknown { system_user, .. } => system_user,
316 Self::BackendBackup { system_user, .. }
317 | Self::BackendHermeticMetrics { system_user }
318 | Self::BackendMetrics { system_user, .. }
319 | Self::BackendSign { system_user, .. }
320 | Self::BackendUpdate { system_user, .. }
321 | Self::HostDownloadNetworkConfig { system_user, .. }
322 | Self::HostShareholder { system_user, .. } => system_user,
323 }
324 }
325
326 pub fn ssh_authorized_keys(&'a self) -> Vec<&'a AuthorizedKeyEntry> {
328 match self {
329 Self::BackendAdmin { .. } | Self::BackendHermeticMetrics { .. } => Vec::new(),
330 Self::BackendBackup {
331 ssh_authorized_key, ..
332 }
333 | Self::BackendMetrics {
334 ssh_authorized_key, ..
335 }
336 | Self::BackendSign {
337 ssh_authorized_key, ..
338 }
339 | Self::HostDownloadNetworkConfig {
340 ssh_authorized_key, ..
341 }
342 | Self::HostShareholder {
343 ssh_authorized_key, ..
344 }
345 | Self::BackendUpdate {
346 ssh_authorized_key, ..
347 } => vec![ssh_authorized_key],
348 Self::Unknown {
349 ssh_authorized_keys,
350 ..
351 } => ssh_authorized_keys.iter().collect::<Vec<_>>(),
352 }
353 }
354}
355
356impl<'a> Display for SystemUserData<'a> {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 write!(f, "system user {} ", self.system_user())?;
359 let ssh_authorized_keys = self.ssh_authorized_keys();
360 if let Self::Unknown { home_dir, .. } = self {
361 write!(f, "in home dir {home_dir:?} ")?;
362 }
363 if !ssh_authorized_keys.is_empty() {
364 write!(
365 f,
366 "with ssh keys {} ",
367 ssh_authorized_keys
368 .iter()
369 .map(ToString::to_string)
370 .collect::<Vec<_>>()
371 .join(", ")
372 )?;
373 }
374
375 write!(
376 f,
377 "{}",
378 match self {
379 Self::BackendAdmin { .. } => "for backend administration",
380 Self::BackendBackup { .. } => "for backend backups",
381 Self::BackendHermeticMetrics { .. } => "for hermetic backend metrics",
382 Self::BackendMetrics { .. } => "for backend metrics",
383 Self::BackendSign { .. } => "for signing using a backend",
384 Self::BackendUpdate { .. } => "for backend updates",
385 Self::HostDownloadNetworkConfig { .. } => "for downloading host network config",
386 Self::HostShareholder { .. } => "for handling shares of a shared secret",
387 Self::Unknown { .. } => "for unknown use",
388 }
389 )
390 }
391}
392
393#[derive(Debug, Eq, PartialEq)]
395pub struct SystemUserHostState<'a> {
396 pub(crate) system_user_data: HashSet<SystemUserData<'a>>,
397}
398
399impl<'a> SystemUserHostState<'a> {
400 pub const STATE_NAME: &'static str = "system";
402
403 pub fn new() -> Result<Self, crate::Error> {
424 let user_data = unsafe { all_users() }.collect::<Vec<_>>();
425 let mut system_user_data = HashSet::new();
426
427 for user in user_data {
428 let user = User::from_name(&user.name().to_string_lossy())
429 .map_err(|_source| Error::InvalidSystemUserName {
430 name: user.name().to_string_lossy().to_string(),
431 })?
432 .ok_or(Error::InvalidSystemUserName {
433 name: user.name().to_string_lossy().to_string(),
434 })?;
435
436 let ssh_authorized_keys = {
438 let mut ssh_authorized_keys = Vec::new();
439
440 let files = [
441 get_ssh_authorized_key_base_dir()
443 .join(format!("signstar-user-{}.authorized_keys", user.name)),
444 get_home_base_dir_path()
446 .join(&user.name)
447 .join(".ssh")
448 .join("authorized_keys"),
449 ];
450
451 for file in files {
452 if file.is_file() {
453 for line in read_to_string(&file)
454 .map_err(|source| crate::Error::IoPath {
455 path: file,
456 context: "reading the file to string",
457 source,
458 })?
459 .lines()
460 {
461 ssh_authorized_keys.push(AuthorizedKeyEntry::from_str(line)?);
462 }
463 }
464 }
465
466 ssh_authorized_keys
467 };
468
469 system_user_data.insert(SystemUserData::Unknown {
470 system_user: SystemUserId::try_from(&user)?,
471 ssh_authorized_keys,
472 home_dir: user.dir,
473 });
474 }
475
476 Ok(Self { system_user_data })
477 }
478
479 pub fn system_user_data(&self) -> &HashSet<SystemUserData<'a>> {
481 &self.system_user_data
482 }
483}
484
485impl<'a> StateOriginInfo for SystemUserHostState<'a> {
486 fn state_name(&self) -> &str {
487 Self::STATE_NAME
488 }
489
490 fn state_origin(&self) -> StateOrigin {
491 StateOrigin::System
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use rstest::rstest;
498 use testresult::TestResult;
499
500 use super::*;
501
502 #[test]
503 fn system_user_id_new_fails() {
504 assert!(SystemUserId::new("üser".to_string()).is_err());
505 }
506
507 #[test]
508 fn authorized_key_entry_new_fails() {
509 assert!(AuthorizedKeyEntry::new("foo".to_string()).is_err());
510 }
511
512 #[test]
513 fn authorized_key_to_string() -> TestResult {
514 let entry = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPkpXKiNhy39A3bZ1u19a5d4sFwYMBkWQyCbzgUfdKBm user@host";
515 let authorized_key = AuthorizedKeyEntry::new(entry.to_string())?;
516
517 assert_eq!(authorized_key.to_string(), entry);
518 Ok(())
519 }
520
521 #[rstest]
522 #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, SystemUserId::new("root".to_string())?)]
523 #[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())?)]
524 #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, SystemUserId::new("hermetic-metrics".to_string())?)]
525 #[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())?)]
526 #[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())?)]
527 #[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())?)]
528 #[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())?)]
529 #[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())?)]
530 #[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())?)]
531 fn system_user_data_system_user<'a>(
532 #[case] system_user_data: SystemUserData<'a>,
533 #[case] system_user: SystemUserId,
534 ) -> TestResult {
535 assert_eq!(system_user_data.system_user(), &system_user);
536 Ok(())
537 }
538
539 #[rstest]
540 #[case::backend_admin(SystemUserData::BackendAdmin{system_user: SystemUserId::new("root".to_string())?}, Vec::new())]
541 #[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()?])]
542 #[case::backend_hermetic_metrics(SystemUserData::BackendHermeticMetrics { system_user: &SystemUserId::new("hermetic-metrics".to_string())?}, Vec::new())]
543 #[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()?])]
544 #[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()?])]
545 #[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()?])]
546 #[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()?])]
547 #[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()?])]
548 #[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()?])]
549 fn system_user_data_ssh_authorized_keys<'a>(
550 #[case] system_user_data: SystemUserData<'a>,
551 #[case] ssh_authorized_keys: Vec<AuthorizedKeyEntry>,
552 ) -> TestResult {
553 assert_eq!(
554 system_user_data.ssh_authorized_keys(),
555 ssh_authorized_keys.iter().collect::<Vec<_>>()
556 );
557 Ok(())
558 }
559
560 #[cfg(target_os = "linux")]
561 #[test]
562 fn system_user_host_state_new_contains_root() -> TestResult {
563 let state = SystemUserHostState::new()?;
564
565 assert!(state.system_user_data.contains(&SystemUserData::Unknown {
566 system_user: SystemUserId::root(),
567 ssh_authorized_keys: Vec::new(),
568 home_dir: PathBuf::from("/root"),
569 }));
570
571 Ok(())
572 }
573}