urma_node/install/
account.rs1use crate::{
2 config::SERVICE_NAME,
3 install::{Error, Runner, System, system::text},
4 platform::Paths,
5};
6use std::{fs, io::Write, path::Path};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct Ids {
10 pub uid: u32,
11 pub gid: u32,
12}
13
14pub struct Account;
15
16impl Account {
17 pub const NAME: &'static str = SERVICE_NAME;
18 const NOLOGIN: [&'static str; 3] = ["/usr/sbin/nologin", "/sbin/nologin", "/usr/bin/nologin"];
19 const NO_SHELL: &'static str = "/bin/false";
20 const ROOT: &'static str = "root";
21}
22
23fn number(field: &str) -> Result<u32, Error> {
24 field
25 .parse()
26 .map_err(|error| Error::Account(format!("bad id {field:?}: {error}")))
27}
28
29impl<R: Runner> System<R> {
30 fn table(&self, file: &str) -> Result<Vec<Vec<String>>, Error> {
31 let content = fs::read_to_string(self.at(Path::new(file)))?;
32 Ok(content
33 .lines()
34 .map(|line| line.split(':').map(str::to_owned).collect::<Vec<_>>())
35 .filter(|fields| fields.len() >= 4)
36 .collect())
37 }
38
39 fn entries(&self, file: &str) -> Result<Vec<Vec<String>>, Error> {
40 Ok(self
41 .table(file)?
42 .into_iter()
43 .filter(|fields| fields[0] == Account::NAME)
44 .collect())
45 }
46
47 fn has(&self, file: &str) -> Result<bool, Error> {
48 Ok(!self.entries(file)?.is_empty())
49 }
50
51 fn ids(&self) -> Result<Ids, Error> {
52 let entries = self.entries("/etc/passwd")?;
53 let Some(fields) = entries.first() else {
54 return Err(Error::Account(format!(
55 "{} is missing from /etc/passwd",
56 Account::NAME
57 )));
58 };
59 Ok(Ids {
60 uid: number(&fields[2])?,
61 gid: number(&fields[3])?,
62 })
63 }
64
65 fn members(&self) -> Result<Vec<String>, Error> {
66 let entries = self.entries("/etc/group")?;
67 let Some(fields) = entries.first() else {
68 return Err(Error::Account(format!(
69 "group {} is missing",
70 Account::NAME
71 )));
72 };
73 Ok(fields[3]
74 .split(',')
75 .filter(|member| !member.is_empty())
76 .map(str::to_owned)
77 .collect())
78 }
79
80 fn nologin(&self) -> &'static str {
81 for shell in Account::NOLOGIN {
82 if self.at(Path::new(shell)).is_file() {
83 return shell;
84 }
85 }
86 Account::NO_SHELL
87 }
88
89 pub fn ensure_account(&mut self) -> Result<Ids, Error> {
90 if self.has("/etc/passwd")? {
91 return self.ids();
92 }
93 let grouped = self.has("/etc/group")?;
94 let home = Paths::linux_under(Path::new("/")).state_dir;
95 let home = text(&home)?.to_owned();
96 let shell = self.nologin();
97 if self.has_program("useradd") {
98 let group: &[&str] = match grouped {
99 true => &["--gid", Account::NAME],
100 false => &["--user-group"],
101 };
102 let mut args = vec!["--system", "--home-dir", &home, "--no-create-home"];
103 args.extend(["--shell", shell]);
104 args.extend(group);
105 args.push(Account::NAME);
106 self.exec("useradd", &args)?;
107 } else if self.has_program("adduser") {
108 if !grouped {
109 self.exec("addgroup", &["-S", Account::NAME])?;
110 }
111 let mut args = vec!["-S", "-D", "-H", "-h", &home, "-s", shell, "-G"];
112 args.extend([Account::NAME, Account::NAME]);
113 self.exec("adduser", &args)?;
114 } else {
115 return Err(Error::Account(
116 "neither useradd nor adduser is installed".into(),
117 ));
118 }
119 self.ids()
120 }
121
122 pub fn remove_account(&mut self) -> Result<(), Error> {
123 if self.has("/etc/passwd")? {
124 let program = match self.has_program("userdel") {
125 true => "userdel",
126 false => "deluser",
127 };
128 self.exec(program, &[Account::NAME])?;
129 }
130 if self.has("/etc/group")? {
131 let program = match self.has_program("groupdel") {
132 true => "groupdel",
133 false => "delgroup",
134 };
135 self.exec(program, &[Account::NAME])?;
136 }
137 Ok(())
138 }
139
140 fn pkexec_invoker(&self) -> Result<Option<String>, Error> {
141 let uid = number(self.host.pkexec_uid.trim())?;
142 for fields in self.table("/etc/passwd")? {
143 if number(&fields[2])? == uid && fields[0] != Account::ROOT {
144 return Ok(Some(fields[0].clone()));
145 }
146 }
147 Ok(None)
148 }
149
150 fn sudo_invoker(&self) -> Result<Option<String>, Error> {
151 let user = self.host.sudo_user.trim();
152 let claimed = self.host.sudo_uid.trim();
153 if user.is_empty() || user == Account::ROOT || claimed.is_empty() {
154 return Ok(None);
155 }
156 let claimed = number(claimed)?;
157 for fields in self.table("/etc/passwd")? {
158 if fields[0] == user && number(&fields[2])? == claimed {
159 return Ok(Some(user.to_owned()));
160 }
161 }
162 Ok(None)
163 }
164
165 fn invoker(&self) -> Result<Option<String>, Error> {
166 match self.host.pkexec_uid.trim().is_empty() {
167 false => self.pkexec_invoker(),
168 true => self.sudo_invoker(),
169 }
170 }
171
172 pub fn join_group(&mut self, out: &mut impl Write) -> Result<(), Error> {
173 let Some(user) = self.invoker()? else {
174 writeln!(
175 out,
176 "To read the node token as your desktop user, run: sudo usermod -a -G {} <user>, then log in again.",
177 Account::NAME
178 )?;
179 return Ok(());
180 };
181 if self.members()?.contains(&user) {
182 return Ok(());
183 }
184 match self.has_program("usermod") {
185 true => self.exec("usermod", &["-a", "-G", Account::NAME, &user])?,
186 false => self.exec("adduser", &[&user, Account::NAME])?,
187 };
188 writeln!(
189 out,
190 "Added {user} to group {}; log in again to read the node token.",
191 Account::NAME
192 )?;
193 Ok(())
194 }
195}