Skip to content

Commit e26c35f

Browse files
committed
WIP: rust: Add sysusers code
Prep for #49 The main thing this is starting to implement is support for "intercepting" `useradd` etc.
1 parent 89486ea commit e26c35f

File tree

3 files changed

+343
-0
lines changed

3 files changed

+343
-0
lines changed

rust/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ glib-sys = "0.6.0"
1313
gio-sys = "0.6.0"
1414
glib = "0.5.0"
1515
tempfile = "3.0.3"
16+
clap = "~2.28"
1617
openat = "0.1.15"
1718
curl = "0.4.14"
1819
c_utf8 = "0.1.0"

rust/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818

1919
extern crate c_utf8;
20+
extern crate clap;
2021
extern crate curl;
2122
extern crate gio_sys;
2223
extern crate glib;
@@ -44,3 +45,5 @@ mod journal;
4445
pub use journal::*;
4546
mod utils;
4647
pub use utils::*;
48+
mod sysusers;
49+
pub use sysusers::*;

rust/src/sysusers.rs

+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/*
2+
* Copyright (C) 2018 Red Hat, Inc.
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17+
*/
18+
19+
use clap::{App, Arg};
20+
use std::io;
21+
22+
#[derive(PartialEq, Debug)]
23+
enum IdSpecification {
24+
Unspecified,
25+
Specified(u32),
26+
Path(String),
27+
}
28+
29+
impl IdSpecification {
30+
fn parse(buf: &str) -> Result<Self, String> {
31+
if buf.starts_with("/") {
32+
Ok(IdSpecification::Path(buf.to_string()))
33+
} else if buf == "-" {
34+
Ok(IdSpecification::Unspecified)
35+
} else {
36+
Ok(IdSpecification::Specified(
37+
buf.parse::<u32>().map_err(|e| e.to_string())?,
38+
))
39+
}
40+
}
41+
}
42+
43+
// For now, we don't parse all of the entry data; we'd need to handle quoting
44+
// etc. See extract-word.c in the systemd source. All we need is the
45+
// user/group → {u,g}id mapping to ensure we're not creating duplicate entries.
46+
#[derive(PartialEq, Debug)]
47+
enum PartialSysuserEntry {
48+
User { name: String, uid: IdSpecification },
49+
Group { name: String, gid: IdSpecification },
50+
GroupMember { uname: String, gname: String },
51+
}
52+
53+
/// The full version that we get from `useradd`.
54+
#[derive(PartialEq, Debug)]
55+
enum SysuserEntry {
56+
User {
57+
name: String,
58+
uid: IdSpecification,
59+
gecos: Option<String>,
60+
homedir: Option<String>,
61+
shell: Option<String>,
62+
},
63+
Group {
64+
name: String,
65+
gid: IdSpecification,
66+
},
67+
GroupMember {
68+
uname: String,
69+
gname: String,
70+
},
71+
}
72+
73+
/// (Partially) parse single a single line from a sysusers.d file
74+
fn parse_entry(line: &str) -> Result<PartialSysuserEntry, String> {
75+
let err = || { format!("Invalid sysusers entry: \"{}\"", line) };
76+
let mut elts = line.split_whitespace().fuse();
77+
match elts.next().ok_or_else(err)? {
78+
"u" => {
79+
let name = elts.next().ok_or_else(err)?.to_string();
80+
let uidstr = elts.next().ok_or_else(err)?;
81+
let uid = IdSpecification::parse(uidstr).map_err(|e| e.to_string())?;
82+
Ok(PartialSysuserEntry::User { name, uid })
83+
}
84+
"g" => {
85+
let name = elts.next().ok_or_else(err)?.to_string();
86+
let gidstr = elts.next().ok_or_else(err)?;
87+
let gid = IdSpecification::parse(gidstr).map_err(|e| e.to_string())?;
88+
Ok(PartialSysuserEntry::Group { name, gid })
89+
}
90+
"m" => {
91+
let uname = elts.next().ok_or_else(err)?.to_string();
92+
let gname = elts.next().ok_or_else(err)?.to_string();
93+
Ok(PartialSysuserEntry::GroupMember { uname, gname })
94+
}
95+
_ => Err(err())
96+
}
97+
}
98+
99+
/// Parse a sysusers.d file (as a stream)
100+
fn parse<I: io::BufRead>(stream: I) -> Result<Vec<PartialSysuserEntry>, String> {
101+
let mut res = Vec::new();
102+
for line in stream.lines() {
103+
let line = line.map_err(|e| e.to_string())?;
104+
if line.starts_with("#") || line.is_empty() {
105+
continue;
106+
}
107+
res.push(parse_entry(&line)?);
108+
}
109+
Ok(res)
110+
}
111+
112+
#[derive(PartialEq, Debug)]
113+
struct UseraddInvocation {
114+
user: SysuserEntry,
115+
groups: Vec<String>,
116+
}
117+
118+
fn useradd<'a, I>(args: I) -> Result<UseraddInvocation, String>
119+
where
120+
I: IntoIterator<Item = &'a str>,
121+
{
122+
let app = App::new("useradd")
123+
.version("0.1")
124+
.about("rpm-ostree useradd wrapper")
125+
.arg(Arg::with_name("system").short("r"))
126+
.arg(Arg::with_name("uid").short("u").takes_value(true))
127+
.arg(Arg::with_name("gid").short("g").takes_value(true))
128+
.arg(Arg::with_name("groups").short("G").takes_value(true))
129+
.arg(Arg::with_name("home-dir").short("d").takes_value(true))
130+
.arg(Arg::with_name("comment").short("c").takes_value(true))
131+
.arg(Arg::with_name("shell").short("s").takes_value(true))
132+
.arg(Arg::with_name("username").takes_value(true).required(true));
133+
let matches = app.get_matches_from(args);
134+
135+
let mut uid = IdSpecification::Unspecified;
136+
if let Some(ref uidstr) = matches.value_of("uid") {
137+
uid = IdSpecification::Specified(uidstr.parse::<u32>().map_err(|e| e.to_string())?);
138+
}
139+
let name = matches.value_of("username").unwrap().to_string();
140+
let gecos = matches.value_of("comment").map(|s| s.to_string());
141+
let homedir = matches.value_of("home-dir").map(|s| s.to_string());
142+
let shell = matches.value_of("shell").map(|s| s.to_string());
143+
144+
let mut groups = vec![];
145+
if let Some(gname) = matches.value_of("gid") {
146+
groups.push(gname.to_string());
147+
}
148+
if let Some(gnames) = matches.value_of("groups") {
149+
for gname in gnames.split(",") {
150+
groups.push(gname.to_string());
151+
}
152+
}
153+
if groups.is_empty() {
154+
groups.push(name.clone())
155+
}
156+
Ok(UseraddInvocation {
157+
user: SysuserEntry::User {
158+
name,
159+
uid,
160+
gecos,
161+
homedir,
162+
shell,
163+
},
164+
groups: groups,
165+
})
166+
}
167+
168+
fn groupadd<'a, I>(args: I) -> Result<SysuserEntry, String>
169+
where
170+
I: IntoIterator<Item = &'a str>,
171+
{
172+
let app = App::new("useradd")
173+
.version("0.1")
174+
.about("rpm-ostree groupadd wrapper")
175+
.arg(Arg::with_name("system").short("r"))
176+
.arg(Arg::with_name("gid").short("g").takes_value(true))
177+
.arg(Arg::with_name("groupname").takes_value(true).required(true));
178+
let matches = app.get_matches_from(args);
179+
180+
let mut gid = IdSpecification::Unspecified;
181+
if let Some(ref gidstr) = matches.value_of("gid") {
182+
gid = IdSpecification::Specified(gidstr.parse::<u32>().map_err(|e| e.to_string())?);
183+
}
184+
let name = matches.value_of("groupname").unwrap().to_string();
185+
Ok(SysuserEntry::Group { name, gid })
186+
}
187+
188+
#[cfg(test)]
189+
mod tests {
190+
use super::*;
191+
192+
// from "man sysusers.d"
193+
static SYSUSERS1: &str = r###"
194+
u httpd 404 "HTTP User"
195+
u authd /usr/bin/authd "Authorization user"
196+
u postgres - "Postgresql Database" /var/lib/pgsql /usr/libexec/postgresdb
197+
g input - -
198+
m authd input
199+
u root 0 "Superuser" /root /bin/zsh
200+
"###;
201+
202+
#[test]
203+
fn test_parse() {
204+
let buf = io::BufReader::new(SYSUSERS1.as_bytes());
205+
let r = parse(buf).unwrap();
206+
assert_eq!(
207+
r[0],
208+
PartialSysuserEntry::User {
209+
name: "httpd".to_string(),
210+
uid: IdSpecification::Specified(404)
211+
}
212+
);
213+
assert_eq!(
214+
r[1],
215+
PartialSysuserEntry::User {
216+
name: "authd".to_string(),
217+
uid: IdSpecification::Path("/usr/bin/authd".to_string())
218+
}
219+
);
220+
assert_eq!(
221+
r[2],
222+
PartialSysuserEntry::User {
223+
name: "postgres".to_string(),
224+
uid: IdSpecification::Unspecified
225+
}
226+
);
227+
assert_eq!(
228+
r[3],
229+
PartialSysuserEntry::Group {
230+
name: "input".to_string(),
231+
gid: IdSpecification::Unspecified
232+
}
233+
);
234+
assert_eq!(
235+
r[4],
236+
PartialSysuserEntry::GroupMember {
237+
uname: "authd".to_string(),
238+
gname: "input".to_string()
239+
}
240+
);
241+
assert_eq!(
242+
r[5],
243+
PartialSysuserEntry::User {
244+
name: "root".to_string(),
245+
uid: IdSpecification::Specified(0)
246+
}
247+
);
248+
}
249+
250+
#[test]
251+
fn test_useradd_basics() {
252+
assert_eq!(
253+
useradd(
254+
vec![
255+
"useradd",
256+
"-c",
257+
"Wesnoth server",
258+
"-s",
259+
"/sbin/nologin",
260+
"-r",
261+
"-d",
262+
"/var/run/wesnothd",
263+
"wesnothd"
264+
].iter()
265+
.map(|v| *v)
266+
).unwrap(),
267+
UseraddInvocation {
268+
user: SysuserEntry::User {
269+
name: "wesnothd".to_string(),
270+
uid: IdSpecification::Unspecified,
271+
gecos: Some("Wesnoth server".to_string()),
272+
shell: Some("/sbin/nologin".to_string()),
273+
homedir: Some("/var/run/wesnothd".to_string()),
274+
},
275+
groups: vec!["wesnothd".to_string()],
276+
}
277+
);
278+
assert_eq!(
279+
useradd(
280+
vec![
281+
"useradd",
282+
"-r",
283+
"-u",
284+
"59",
285+
"-g",
286+
"tss",
287+
"-d",
288+
"/dev/null",
289+
"-s",
290+
"/sbin/nologin",
291+
"-c",
292+
"comment",
293+
"tss"
294+
].iter()
295+
.map(|v| *v)
296+
).unwrap(),
297+
UseraddInvocation {
298+
user: SysuserEntry::User {
299+
name: "tss".to_string(),
300+
uid: IdSpecification::Specified(59),
301+
gecos: Some("comment".to_string()),
302+
shell: Some("/sbin/nologin".to_string()),
303+
homedir: Some("/dev/null".to_string()),
304+
},
305+
groups: vec!["tss".to_string()]
306+
}
307+
);
308+
}
309+
310+
#[test]
311+
fn test_groupadd_basics() {
312+
assert_eq!(
313+
groupadd(vec!["groupadd", "-r", "wireshark",].iter().map(|v| *v)).unwrap(),
314+
SysuserEntry::Group {
315+
name: "wireshark".to_string(),
316+
gid: IdSpecification::Unspecified,
317+
},
318+
);
319+
assert_eq!(
320+
groupadd(
321+
vec!["groupadd", "-r", "-g", "112", "vhostmd",]
322+
.iter()
323+
.map(|v| *v)
324+
).unwrap(),
325+
SysuserEntry::Group {
326+
name: "vhostmd".to_string(),
327+
gid: IdSpecification::Specified(112),
328+
},
329+
);
330+
}
331+
}
332+
333+
mod ffi {
334+
use super::*;
335+
336+
#[no_mangle]
337+
pub extern "C" fn ror_sysusers_main() {}
338+
}
339+
pub use self::ffi::*;

0 commit comments

Comments
 (0)