-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
core: wrap and intercept
groupadd
calls in scriptlets
This re-routes `groupadd` calls in scriptlets, in order to inspect group creation by RPMs and automatically generate corresponding sysusers.d fragments. It uses a new hidden `scriptlet-intercept` subcommand, which can be in re-used in future to intercept additional commands (e.g. `useradd`).
- Loading branch information
Showing
7 changed files
with
247 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,4 +3,5 @@ | |
|
||
pub(crate) mod apply_live; | ||
pub(crate) mod compose; | ||
pub mod scriptlet_intercept; | ||
pub mod usroverlay; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
//! CLI handler for intercepted `groupadd`. | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
use anyhow::Result; | ||
use cap_std::fs::Dir; | ||
use clap::{Arg, Command}; | ||
use std::io::Write; | ||
|
||
/// Entrypoint for (the rpm-ostree implementation of) `groupadd`. | ||
pub(crate) fn entrypoint(args: &[&str]) -> Result<()> { | ||
fail::fail_point!("intercept_groupadd_ok", |_| Ok(())); | ||
|
||
// This parses the same CLI surface as the real `groupadd`, | ||
// but in the end we only extract the group name and (if | ||
// present) the static GID. | ||
let matches = cli_cmd().get_matches_from(args); | ||
let gid: Option<u32> = match matches.value_of("gid") { | ||
None => None, | ||
Some(s) => Some(s.parse()?), | ||
}; | ||
let groupname = matches | ||
.value_of("groupname") | ||
.expect("missing required groupname"); | ||
|
||
let rootdir = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; | ||
generate_sysusers_fragment(&rootdir, groupname, gid)?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// CLI parser, matches <https://linux.die.net/man/8/groupadd>. | ||
fn cli_cmd() -> Command<'static> { | ||
let name = "groupadd"; | ||
Command::new(name) | ||
.bin_name(name) | ||
.about("create a new group") | ||
.arg(Arg::new("force").short('f').long("force")) | ||
.arg( | ||
Arg::new("help") | ||
.short('h') | ||
.long("help") | ||
.action(clap::ArgAction::Help), | ||
) | ||
.arg(Arg::new("gid").short('g').long("gid").takes_value(true)) | ||
.arg(Arg::new("key").short('K').long("key").takes_value(true)) | ||
.arg(Arg::new("allow_duplicates").short('o').long("non-unique")) | ||
.arg( | ||
Arg::new("password") | ||
.short('p') | ||
.long("password") | ||
.takes_value(true), | ||
) | ||
.arg(Arg::new("system").short('r').long("system")) | ||
.arg( | ||
Arg::new("chroot_dir") | ||
.short('R') | ||
.long("root") | ||
.takes_value(true), | ||
) | ||
.arg( | ||
Arg::new("prefix_dir") | ||
.short('P') | ||
.long("prefix") | ||
.takes_value(true), | ||
) | ||
.arg(Arg::new("users").short('U').long("users").takes_value(true)) | ||
.arg(Arg::new("groupname").required(true)) | ||
} | ||
|
||
/// Write a sysusers.d configuration fragment for the given group. | ||
/// | ||
/// This returns whether a new fragment has been actually written | ||
/// to disk. | ||
fn generate_sysusers_fragment(rootdir: &Dir, groupname: &str, gid: Option<u32>) -> Result<bool> { | ||
static SYSUSERS_DIR: &str = "usr/lib/sysusers.d"; | ||
|
||
// The filename of the configuration fragment is in fact a public | ||
// API, because users may have masked it in /etc. Do not change this. | ||
let filename = format!("30-pkg-group-{groupname}.conf"); | ||
|
||
rootdir.create_dir_all(SYSUSERS_DIR)?; | ||
let conf_dir = rootdir.open_dir(SYSUSERS_DIR)?; | ||
if conf_dir.exists(&filename) { | ||
return Ok(false); | ||
} | ||
|
||
let mut fragment = conf_dir.create(filename)?; | ||
let gid_value = gid | ||
.map(|id| id.to_string()) | ||
.unwrap_or_else(|| "-".to_string()); | ||
writeln!(fragment, "# Generated by rpm-ostree")?; | ||
writeln!(fragment, "g {groupname} {gid_value}")?; | ||
|
||
Ok(true) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
use std::io::Read; | ||
|
||
#[test] | ||
fn test_clap_cmd() { | ||
cli_cmd().debug_assert(); | ||
|
||
let cmd = cli_cmd(); | ||
let static_gid = ["/usr/sbin/groupadd", "-g", "23", "squid"]; | ||
let matches = cmd.try_get_matches_from(static_gid).unwrap(); | ||
assert_eq!(matches.value_of("gid"), Some("23")); | ||
assert_eq!(matches.value_of("groupname"), Some("squid")); | ||
|
||
let cmd = cli_cmd(); | ||
let dynamic_gid = ["/usr/sbin/groupadd", "-r", "chrony"]; | ||
let matches = cmd.try_get_matches_from(dynamic_gid).unwrap(); | ||
assert!(matches.contains_id("system")); | ||
assert_eq!(matches.value_of("gid"), None); | ||
assert_eq!(matches.value_of("groupname"), Some("chrony")); | ||
|
||
let err_cases = [vec!["/usr/sbin/groupadd"]]; | ||
for input in err_cases { | ||
let cmd = cli_cmd(); | ||
cmd.try_get_matches_from(input).unwrap_err(); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_fragment_generation() { | ||
let tmpdir = cap_tempfile::tempdir(cap_tempfile::ambient_authority()).unwrap(); | ||
|
||
let groups = [ | ||
("foo", Some(42), true, "42"), | ||
("foo", None, false, "42"), | ||
("bar", None, true, "-"), | ||
]; | ||
for entry in groups { | ||
let generated = generate_sysusers_fragment(&tmpdir, entry.0, entry.1).unwrap(); | ||
assert_eq!(generated, entry.2, "{:?}", entry); | ||
|
||
let path = format!("usr/lib/sysusers.d/10-pkg-group-{}.conf", entry.0); | ||
assert!(tmpdir.is_file(&path)); | ||
|
||
let mut fragment = tmpdir.open(&path).unwrap(); | ||
let mut content = String::new(); | ||
fragment.read_to_string(&mut content).unwrap(); | ||
let expected = format!("# Generated by rpm-ostree\ng {} {}\n", entry.0, entry.3); | ||
assert_eq!(content, expected) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
//! CLI handler for `rpm-ostree scriplet-intercept`. | ||
// SPDX-License-Identifier: Apache-2.0 OR MIT | ||
|
||
mod groupadd; | ||
use anyhow::{bail, Result}; | ||
|
||
/// Entrypoint for `rpm-ostree scriplet-intercept`. | ||
pub fn entrypoint(args: &[&str]) -> Result<()> { | ||
// Here we expect arguments that look like | ||
// `rpm-ostree scriptlet-intercept <command> -- <rest>` | ||
if args.len() < 4 || args[3] != "--" { | ||
bail!("Invalid arguments"); | ||
} | ||
|
||
let orig_command = args[2]; | ||
let rest = &args[4..]; | ||
match orig_command { | ||
"groupadd" => groupadd::entrypoint(rest), | ||
x => bail!("Unable to intercept command '{}'", x), | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_entrypoint_args() { | ||
// Short-circuit groupadd logic, this test is only meant to check CLI parsing. | ||
let _guard = fail::FailScenario::setup(); | ||
fail::cfg("intercept_groupadd_ok", "return").unwrap(); | ||
|
||
let err_cases = [ | ||
vec![], | ||
vec!["rpm-ostree", "install"], | ||
vec!["rpm-ostree", "scriptlet-intercept", "groupadd"], | ||
vec!["rpm-ostree", "scriptlet-intercept", "foo", "--"], | ||
]; | ||
for input in &err_cases { | ||
entrypoint(input).unwrap_err(); | ||
} | ||
|
||
let ok_cases = [vec!["rpm-ostree", "scriptlet-intercept", "groupadd", "--"]]; | ||
for input in &ok_cases { | ||
entrypoint(input).unwrap(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
#!/usr/bin/bash | ||
# Used by rpmostree-core.c to intercept `groupadd` calls. | ||
# We want to learn about group creation and distinguish between | ||
# static and dynamic GIDs, in order to auto-generate relevant | ||
# `sysusers.d` fragments. | ||
# See also https://github.com/coreos/rpm-ostree/issues/3762 | ||
|
||
rpm-ostree scriptlet-intercept groupadd -- "$0" "$@" | ||
|
||
# Forward to the real `groupadd` for group creation. | ||
exec /usr/sbin/groupadd.rpmostreesave "$@" |