Merge pull request #2371 from jhscheer/fix_get_groups

uucore: fix order of group IDs return from entries::get_groups()
This commit is contained in:
Terts Diepraam 2021-06-08 21:25:12 +02:00 committed by GitHub
commit 05f65b05ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 162 additions and 66 deletions

View file

@ -349,7 +349,7 @@ sha1 = { version="0.6", features=["std"] }
tempfile = "3.2.0"
time = "0.1"
unindent = "0.1"
uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries"] }
uucore = { version=">=0.0.8", package="uucore", path="src/uucore", features=["entries", "process"] }
walkdir = "2.2"
atty = "0.2.14"

View file

@ -10,7 +10,7 @@
#[macro_use]
extern crate uucore;
use uucore::entries::{get_groups, gid2grp, Locate, Passwd};
use uucore::entries::{get_groups_gnu, gid2grp, Locate, Passwd};
use clap::{crate_version, App, Arg};
@ -35,7 +35,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
None => {
println!(
"{}",
get_groups()
get_groups_gnu(None)
.unwrap()
.iter()
.map(|&g| gid2grp(g).unwrap())

View file

@ -10,7 +10,7 @@
// http://ftp-archive.freebsd.org/mirror/FreeBSD-Archive/old-releases/i386/1.0-RELEASE/ports/shellutils/src/id.c
// http://www.opensource.apple.com/source/shell_cmds/shell_cmds-118/id/id.c
// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag
// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag
#![allow(non_camel_case_types)]
#![allow(dead_code)]
@ -79,7 +79,7 @@ static OPT_GROUPS: &str = "groups";
static OPT_HUMAN_READABLE: &str = "human-readable";
static OPT_NAME: &str = "name";
static OPT_PASSWORD: &str = "password";
static OPT_REAL_ID: &str = "real-id";
static OPT_REAL_ID: &str = "real";
static ARG_USERS: &str = "users";
@ -135,7 +135,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.arg(
Arg::with_name(OPT_REAL_ID)
.short("r")
.help("Display the real ID for the -g and -u options"),
.long(OPT_REAL_ID)
.help(
"Display the real ID for the -G, -g and -u options instead of the effective ID.",
),
)
.arg(Arg::with_name(ARG_USERS).multiple(true).takes_value(true))
.get_matches_from(args);
@ -162,6 +165,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
let nflag = matches.is_present(OPT_NAME);
let uflag = matches.is_present(OPT_EFFECTIVE_USER);
let gflag = matches.is_present(OPT_GROUP);
let gsflag = matches.is_present(OPT_GROUPS);
let rflag = matches.is_present(OPT_REAL_ID);
if gflag {
@ -194,26 +198,23 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
return 0;
}
if matches.is_present(OPT_GROUPS) {
if gsflag {
let id = possible_pw
.map(|p| p.gid())
.unwrap_or(if rflag { getgid() } else { getegid() });
println!(
"{}",
if nflag {
possible_pw
.map(|p| p.belongs_to())
.unwrap_or_else(|| entries::get_groups().unwrap())
.iter()
.map(|&id| entries::gid2grp(id).unwrap())
.collect::<Vec<_>>()
.join(" ")
} else {
possible_pw
.map(|p| p.belongs_to())
.unwrap_or_else(|| entries::get_groups().unwrap())
.iter()
.map(|&id| id.to_string())
.collect::<Vec<_>>()
.join(" ")
}
possible_pw
.map(|p| p.belongs_to())
.unwrap_or_else(|| entries::get_groups_gnu(Some(id)).unwrap())
.iter()
.map(|&id| if nflag {
entries::gid2grp(id).unwrap_or_else(|_| id.to_string())
} else {
id.to_string()
})
.collect::<Vec<_>>()
.join(" ")
);
return 0;
}
@ -280,7 +281,7 @@ fn pretty(possible_pw: Option<Passwd>) {
println!(
"groups\t{}",
entries::get_groups()
entries::get_groups_gnu(None)
.unwrap()
.iter()
.map(|&gr| entries::gid2grp(gr).unwrap())

View file

@ -5,7 +5,7 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups
// spell-checker:ignore (vars) Passwd cstr fnam gecos ngroups egid
//! Get password/group file entry
//!
@ -72,6 +72,41 @@ pub fn get_groups() -> IOResult<Vec<gid_t>> {
}
}
/// The list of group IDs returned from GNU's `groups` and GNU's `id --groups`
/// starts with the effective group ID (egid).
/// This is a wrapper for `get_groups()` to mimic this behavior.
///
/// If `arg_id` is `None` (default), `get_groups_gnu` moves the effective
/// group id (egid) to the first entry in the returned Vector.
/// If `arg_id` is `Some(x)`, `get_groups_gnu` moves the id with value `x`
/// to the first entry in the returned Vector. This might be necessary
/// for `id --groups --real` if `gid` and `egid` are not equal.
///
/// From: https://www.man7.org/linux/man-pages/man3/getgroups.3p.html
/// As implied by the definition of supplementary groups, the
/// effective group ID may appear in the array returned by
/// getgroups() or it may be returned only by getegid(). Duplication
/// may exist, but the application needs to call getegid() to be sure
/// of getting all of the information. Various implementation
/// variations and administrative sequences cause the set of groups
/// appearing in the result of getgroups() to vary in order and as to
/// whether the effective group ID is included, even when the set of
/// groups is the same (in the mathematical sense of ``set''). (The
/// history of a process and its parents could affect the details of
/// the result.)
pub fn get_groups_gnu(arg_id: Option<u32>) -> IOResult<Vec<gid_t>> {
let mut groups = get_groups()?;
let egid = arg_id.unwrap_or_else(crate::features::process::getegid);
if !groups.is_empty() && *groups.first().unwrap() == egid {
return Ok(groups);
} else if let Some(index) = groups.iter().position(|&x| x == egid) {
groups.remove(index);
}
groups.insert(0, egid);
Ok(groups)
}
#[derive(Copy, Clone)]
pub struct Passwd {
inner: passwd,
}
@ -268,3 +303,18 @@ pub fn usr2uid(name: &str) -> IOResult<uid_t> {
pub fn grp2gid(name: &str) -> IOResult<gid_t> {
Group::locate(name).map(|p| p.gid())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_entries_get_groups_gnu() {
if let Ok(mut groups) = get_groups() {
if let Some(last) = groups.pop() {
groups.insert(0, last);
assert_eq!(get_groups_gnu(Some(last)).unwrap(), groups);
}
}
}
}

View file

@ -1,41 +1,53 @@
use crate::common::util::*;
#[test]
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
fn test_groups() {
let result = new_ucmd!().run();
println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_str());
if is_ci() && result.stdout_str().trim().is_empty() {
// In the CI, some server are failing to return the group.
// As seems to be a configuration issue, ignoring it
return;
if !is_ci() {
new_ucmd!().succeeds().stdout_is(expected_result(&[]));
} else {
// TODO: investigate how this could be tested in CI
// stderr = groups: cannot find name for group ID 116
println!("test skipped:");
}
result.success();
assert!(!result.stdout_str().trim().is_empty());
}
#[test]
fn test_groups_arg() {
// get the username with the "id -un" command
let result = TestScenario::new("id").ucmd_keepenv().arg("-un").run();
println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_str());
let s1 = String::from(result.stdout_str().trim());
if is_ci() && s1.parse::<f64>().is_ok() {
// In the CI, some server are failing to return id -un.
// So, if we are getting a uid, just skip this test
// As seems to be a configuration issue, ignoring it
#[cfg(any(target_os = "linux"))]
#[ignore = "fixme: 'groups USERNAME' needs more debugging"]
fn test_groups_username() {
let scene = TestScenario::new(util_name!());
let whoami_result = scene.cmd("whoami").run();
let username = if whoami_result.succeeded() {
whoami_result.stdout_move_str()
} else if is_ci() {
String::from("docker")
} else {
println!("test skipped:");
return;
}
};
println!("result.stdout = {}", result.stdout_str());
println!("result.stderr = {}", result.stderr_str());
result.success();
assert!(!result.stdout_str().is_empty());
let username = result.stdout_str().trim();
// TODO: stdout should be in the form: "username : group1 group2 group3"
// call groups with the user name to check that we
// are getting something
new_ucmd!().arg(username).succeeds();
assert!(!result.stdout_str().is_empty());
scene
.ucmd()
.arg(&username)
.succeeds()
.stdout_is(expected_result(&[&username]));
}
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
fn expected_result(args: &[&str]) -> String {
#[cfg(target_os = "linux")]
let util_name = util_name!();
#[cfg(target_vendor = "apple")]
let util_name = format!("g{}", util_name!());
TestScenario::new(&util_name)
.cmd_keepenv(util_name)
.env("LANGUAGE", "C")
.args(args)
.succeeds()
.stdout_move_str()
}

View file

@ -104,19 +104,23 @@ fn test_id_group() {
}
#[test]
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
fn test_id_groups() {
let scene = TestScenario::new(util_name!());
let result = scene.ucmd().arg("-G").succeeds();
let groups = result.stdout_str().trim().split_whitespace();
for s in groups {
assert!(s.parse::<f64>().is_ok());
}
let result = scene.ucmd().arg("--groups").succeeds();
let groups = result.stdout_str().trim().split_whitespace();
for s in groups {
assert!(s.parse::<f64>().is_ok());
for g_flag in &["-G", "--groups"] {
scene
.ucmd()
.arg(g_flag)
.succeeds()
.stdout_is(expected_result(&[g_flag], false));
for &r_flag in &["-r", "--real"] {
let args = [g_flag, r_flag];
scene
.ucmd()
.args(&args)
.succeeds()
.stdout_is(expected_result(&args, false));
}
}
}
@ -167,3 +171,32 @@ fn test_id_password_style() {
assert!(result.stdout_str().starts_with(&username));
}
#[cfg(any(target_vendor = "apple", target_os = "linux"))]
fn expected_result(args: &[&str], exp_fail: bool) -> String {
#[cfg(target_os = "linux")]
let util_name = util_name!();
#[cfg(target_vendor = "apple")]
let util_name = format!("g{}", util_name!());
let result = if !exp_fail {
TestScenario::new(&util_name)
.cmd_keepenv(util_name)
.env("LANGUAGE", "C")
.args(args)
.succeeds()
.stdout_move_str()
} else {
TestScenario::new(&util_name)
.cmd_keepenv(util_name)
.env("LANGUAGE", "C")
.args(args)
.fails()
.stderr_move_str()
};
return if cfg!(target_os = "macos") && result.starts_with("gid") {
result[1..].to_string()
} else {
result
};
}