uu_id/
id.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (ToDO) asid auditid auditinfo auid cstr egid emod euid getaudit getlogin gflag nflag pline rflag termid uflag gsflag zflag cflag
7
8// README:
9// This was originally based on BSD's `id`
10// (noticeable in functionality, usage text, options text, etc.)
11// and synced with:
12//  http://ftp-archive.freebsd.org/mirror/FreeBSD-Archive/old-releases/i386/1.0-RELEASE/ports/shellutils/src/id.c
13//  http://www.opensource.apple.com/source/shell_cmds/shell_cmds-118/id/id.c
14//
15// * This was partially rewritten in order for stdout/stderr/exit_code
16//   to be conform with GNU coreutils (8.32) test suite for `id`.
17//
18// * This supports multiple users (a feature that was introduced in coreutils 8.31)
19//
20// * This passes GNU's coreutils Test suite (8.32)
21//   for "tests/id/uid.sh" and "tests/id/zero/sh".
22//
23// * Option '--zero' does not exist for BSD's `id`, therefore '--zero' is only
24//   allowed together with other options that are available on GNU's `id`.
25//
26// * Help text based on BSD's `id` manpage and GNU's `id` manpage.
27//
28// * This passes GNU's coreutils Test suite (8.32) for "tests/id/context.sh" if compiled with
29//   `--features feat_selinux`. It should also pass "tests/id/no-context.sh", but that depends on
30//   `uu_ls -Z` being implemented and therefore fails at the moment
31//
32
33#![allow(non_camel_case_types)]
34#![allow(dead_code)]
35
36use clap::{crate_version, Arg, ArgAction, Command};
37use std::ffi::CStr;
38use uucore::display::Quotable;
39use uucore::entries::{self, Group, Locate, Passwd};
40use uucore::error::UResult;
41use uucore::error::{set_exit_code, USimpleError};
42pub use uucore::libc;
43use uucore::libc::{getlogin, uid_t};
44use uucore::line_ending::LineEnding;
45use uucore::process::{getegid, geteuid, getgid, getuid};
46use uucore::{format_usage, help_about, help_section, help_usage, show_error};
47
48macro_rules! cstr2cow {
49    ($v:expr) => {
50        unsafe { CStr::from_ptr($v).to_string_lossy() }
51    };
52}
53
54const ABOUT: &str = help_about!("id.md");
55const USAGE: &str = help_usage!("id.md");
56const AFTER_HELP: &str = help_section!("after help", "id.md");
57
58#[cfg(not(feature = "selinux"))]
59static CONTEXT_HELP_TEXT: &str = "print only the security context of the process (not enabled)";
60#[cfg(feature = "selinux")]
61static CONTEXT_HELP_TEXT: &str = "print only the security context of the process";
62
63mod options {
64    pub const OPT_AUDIT: &str = "audit"; // GNU's id does not have this
65    pub const OPT_CONTEXT: &str = "context";
66    pub const OPT_EFFECTIVE_USER: &str = "user";
67    pub const OPT_GROUP: &str = "group";
68    pub const OPT_GROUPS: &str = "groups";
69    pub const OPT_HUMAN_READABLE: &str = "human-readable"; // GNU's id does not have this
70    pub const OPT_NAME: &str = "name";
71    pub const OPT_PASSWORD: &str = "password"; // GNU's id does not have this
72    pub const OPT_REAL_ID: &str = "real";
73    pub const OPT_ZERO: &str = "zero"; // BSD's id does not have this
74    pub const ARG_USERS: &str = "USER";
75}
76
77struct Ids {
78    uid: u32,  // user id
79    gid: u32,  // group id
80    euid: u32, // effective uid
81    egid: u32, // effective gid
82}
83
84struct State {
85    nflag: bool,  // --name
86    uflag: bool,  // --user
87    gflag: bool,  // --group
88    gsflag: bool, // --groups
89    rflag: bool,  // --real
90    zflag: bool,  // --zero
91    cflag: bool,  // --context
92    selinux_supported: bool,
93    ids: Option<Ids>,
94    // The behavior for calling GNU's `id` and calling GNU's `id $USER` is similar but different.
95    // * The SELinux context is only displayed without a specified user.
96    // * The `getgroups` system call is only used without a specified user, this causes
97    //   the order of the displayed groups to be different between `id` and `id $USER`.
98    //
99    // Example:
100    // $ strace -e getgroups id -G $USER
101    // 1000 10 975 968
102    // +++ exited with 0 +++
103    // $ strace -e getgroups id -G
104    // getgroups(0, NULL)                      = 4
105    // getgroups(4, [10, 968, 975, 1000])      = 4
106    // 1000 10 968 975
107    // +++ exited with 0 +++
108    user_specified: bool,
109}
110
111#[uucore::main]
112#[allow(clippy::cognitive_complexity)]
113pub fn uumain(args: impl uucore::Args) -> UResult<()> {
114    let matches = uu_app().after_help(AFTER_HELP).try_get_matches_from(args)?;
115
116    let users: Vec<String> = matches
117        .get_many::<String>(options::ARG_USERS)
118        .map(|v| v.map(ToString::to_string).collect())
119        .unwrap_or_default();
120
121    let mut state = State {
122        nflag: matches.get_flag(options::OPT_NAME),
123        uflag: matches.get_flag(options::OPT_EFFECTIVE_USER),
124        gflag: matches.get_flag(options::OPT_GROUP),
125        gsflag: matches.get_flag(options::OPT_GROUPS),
126        rflag: matches.get_flag(options::OPT_REAL_ID),
127        zflag: matches.get_flag(options::OPT_ZERO),
128        cflag: matches.get_flag(options::OPT_CONTEXT),
129
130        selinux_supported: {
131            #[cfg(feature = "selinux")]
132            {
133                selinux::kernel_support() != selinux::KernelSupport::Unsupported
134            }
135            #[cfg(not(feature = "selinux"))]
136            {
137                false
138            }
139        },
140        user_specified: !users.is_empty(),
141        ids: None,
142    };
143
144    let default_format = {
145        // "default format" is when none of '-ugG' was used
146        !(state.uflag || state.gflag || state.gsflag)
147    };
148
149    if (state.nflag || state.rflag) && default_format && !state.cflag {
150        return Err(USimpleError::new(
151            1,
152            "cannot print only names or real IDs in default format",
153        ));
154    }
155    if state.zflag && default_format && !state.cflag {
156        // NOTE: GNU test suite "id/zero.sh" needs this stderr output:
157        return Err(USimpleError::new(
158            1,
159            "option --zero not permitted in default format",
160        ));
161    }
162    if state.user_specified && state.cflag {
163        return Err(USimpleError::new(
164            1,
165            "cannot print security context when user specified",
166        ));
167    }
168
169    let delimiter = {
170        if state.zflag {
171            "\0".to_string()
172        } else {
173            " ".to_string()
174        }
175    };
176    let line_ending = LineEnding::from_zero_flag(state.zflag);
177
178    if state.cflag {
179        if state.selinux_supported {
180            // print SElinux context and exit
181            #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "selinux"))]
182            if let Ok(context) = selinux::SecurityContext::current(false) {
183                let bytes = context.as_bytes();
184                print!("{}{}", String::from_utf8_lossy(bytes), line_ending);
185            } else {
186                // print error because `cflag` was explicitly requested
187                return Err(USimpleError::new(1, "can't get process context"));
188            }
189            return Ok(());
190        } else {
191            return Err(USimpleError::new(
192                1,
193                "--context (-Z) works only on an SELinux-enabled kernel",
194            ));
195        }
196    }
197
198    for i in 0..=users.len() {
199        let possible_pw = if state.user_specified {
200            match Passwd::locate(users[i].as_str()) {
201                Ok(p) => Some(p),
202                Err(_) => {
203                    show_error!("{}: no such user", users[i].quote());
204                    set_exit_code(1);
205                    if i + 1 >= users.len() {
206                        break;
207                    } else {
208                        continue;
209                    }
210                }
211            }
212        } else {
213            None
214        };
215
216        // GNU's `id` does not support the flags: -p/-P/-A.
217        if matches.get_flag(options::OPT_PASSWORD) {
218            // BSD's `id` ignores all but the first specified user
219            pline(possible_pw.as_ref().map(|v| v.uid));
220            return Ok(());
221        };
222        if matches.get_flag(options::OPT_HUMAN_READABLE) {
223            // BSD's `id` ignores all but the first specified user
224            pretty(possible_pw);
225            return Ok(());
226        }
227        if matches.get_flag(options::OPT_AUDIT) {
228            // BSD's `id` ignores specified users
229            auditid();
230            return Ok(());
231        }
232
233        let (uid, gid) = possible_pw.as_ref().map(|p| (p.uid, p.gid)).unwrap_or((
234            if state.rflag { getuid() } else { geteuid() },
235            if state.rflag { getgid() } else { getegid() },
236        ));
237        state.ids = Some(Ids {
238            uid,
239            gid,
240            euid: geteuid(),
241            egid: getegid(),
242        });
243
244        if state.gflag {
245            print!(
246                "{}",
247                if state.nflag {
248                    entries::gid2grp(gid).unwrap_or_else(|_| {
249                        show_error!("cannot find name for group ID {}", gid);
250                        set_exit_code(1);
251                        gid.to_string()
252                    })
253                } else {
254                    gid.to_string()
255                }
256            );
257        }
258
259        if state.uflag {
260            print!(
261                "{}",
262                if state.nflag {
263                    entries::uid2usr(uid).unwrap_or_else(|_| {
264                        show_error!("cannot find name for user ID {}", uid);
265                        set_exit_code(1);
266                        uid.to_string()
267                    })
268                } else {
269                    uid.to_string()
270                }
271            );
272        }
273
274        let groups = entries::get_groups_gnu(Some(gid)).unwrap();
275        let groups = if state.user_specified {
276            possible_pw.as_ref().map(|p| p.belongs_to()).unwrap()
277        } else {
278            groups.clone()
279        };
280
281        if state.gsflag {
282            print!(
283                "{}{}",
284                groups
285                    .iter()
286                    .map(|&id| {
287                        if state.nflag {
288                            entries::gid2grp(id).unwrap_or_else(|_| {
289                                show_error!("cannot find name for group ID {}", id);
290                                set_exit_code(1);
291                                id.to_string()
292                            })
293                        } else {
294                            id.to_string()
295                        }
296                    })
297                    .collect::<Vec<_>>()
298                    .join(&delimiter),
299                // NOTE: this is necessary to pass GNU's "tests/id/zero.sh":
300                if state.zflag && state.user_specified && users.len() > 1 {
301                    "\0"
302                } else {
303                    ""
304                }
305            );
306        }
307
308        if default_format {
309            id_print(&state, &groups);
310        }
311        print!("{line_ending}");
312
313        if i + 1 >= users.len() {
314            break;
315        }
316    }
317
318    Ok(())
319}
320
321pub fn uu_app() -> Command {
322    Command::new(uucore::util_name())
323        .version(crate_version!())
324        .about(ABOUT)
325        .override_usage(format_usage(USAGE))
326        .infer_long_args(true)
327        .args_override_self(true)
328        .arg(
329            Arg::new(options::OPT_AUDIT)
330                .short('A')
331                .conflicts_with_all([
332                    options::OPT_GROUP,
333                    options::OPT_EFFECTIVE_USER,
334                    options::OPT_HUMAN_READABLE,
335                    options::OPT_PASSWORD,
336                    options::OPT_GROUPS,
337                    options::OPT_ZERO,
338                ])
339                .help(
340                    "Display the process audit user ID and other process audit properties,\n\
341                      which requires privilege (not available on Linux).",
342                )
343                .action(ArgAction::SetTrue),
344        )
345        .arg(
346            Arg::new(options::OPT_EFFECTIVE_USER)
347                .short('u')
348                .long(options::OPT_EFFECTIVE_USER)
349                .conflicts_with(options::OPT_GROUP)
350                .help("Display only the effective user ID as a number.")
351                .action(ArgAction::SetTrue),
352        )
353        .arg(
354            Arg::new(options::OPT_GROUP)
355                .short('g')
356                .long(options::OPT_GROUP)
357                .conflicts_with(options::OPT_EFFECTIVE_USER)
358                .help("Display only the effective group ID as a number")
359                .action(ArgAction::SetTrue),
360        )
361        .arg(
362            Arg::new(options::OPT_GROUPS)
363                .short('G')
364                .long(options::OPT_GROUPS)
365                .conflicts_with_all([
366                    options::OPT_GROUP,
367                    options::OPT_EFFECTIVE_USER,
368                    options::OPT_CONTEXT,
369                    options::OPT_HUMAN_READABLE,
370                    options::OPT_PASSWORD,
371                    options::OPT_AUDIT,
372                ])
373                .help(
374                    "Display only the different group IDs as white-space separated numbers, \
375                      in no particular order.",
376                )
377                .action(ArgAction::SetTrue),
378        )
379        .arg(
380            Arg::new(options::OPT_HUMAN_READABLE)
381                .short('p')
382                .help("Make the output human-readable. Each display is on a separate line.")
383                .action(ArgAction::SetTrue),
384        )
385        .arg(
386            Arg::new(options::OPT_NAME)
387                .short('n')
388                .long(options::OPT_NAME)
389                .help(
390                    "Display the name of the user or group ID for the -G, -g and -u options \
391                      instead of the number.\nIf any of the ID numbers cannot be mapped into \
392                      names, the number will be displayed as usual.",
393                )
394                .action(ArgAction::SetTrue),
395        )
396        .arg(
397            Arg::new(options::OPT_PASSWORD)
398                .short('P')
399                .help("Display the id as a password file entry.")
400                .conflicts_with(options::OPT_HUMAN_READABLE)
401                .action(ArgAction::SetTrue),
402        )
403        .arg(
404            Arg::new(options::OPT_REAL_ID)
405                .short('r')
406                .long(options::OPT_REAL_ID)
407                .help(
408                    "Display the real ID for the -G, -g and -u options instead of \
409                      the effective ID.",
410                )
411                .action(ArgAction::SetTrue),
412        )
413        .arg(
414            Arg::new(options::OPT_ZERO)
415                .short('z')
416                .long(options::OPT_ZERO)
417                .help(
418                    "delimit entries with NUL characters, not whitespace;\n\
419                      not permitted in default format",
420                )
421                .action(ArgAction::SetTrue),
422        )
423        .arg(
424            Arg::new(options::OPT_CONTEXT)
425                .short('Z')
426                .long(options::OPT_CONTEXT)
427                .conflicts_with_all([options::OPT_GROUP, options::OPT_EFFECTIVE_USER])
428                .help(CONTEXT_HELP_TEXT)
429                .action(ArgAction::SetTrue),
430        )
431        .arg(
432            Arg::new(options::ARG_USERS)
433                .action(ArgAction::Append)
434                .value_name(options::ARG_USERS)
435                .value_hint(clap::ValueHint::Username),
436        )
437}
438
439fn pretty(possible_pw: Option<Passwd>) {
440    if let Some(p) = possible_pw {
441        print!("uid\t{}\ngroups\t", p.name);
442        println!(
443            "{}",
444            p.belongs_to()
445                .iter()
446                .map(|&gr| entries::gid2grp(gr).unwrap())
447                .collect::<Vec<_>>()
448                .join(" ")
449        );
450    } else {
451        let login = cstr2cow!(getlogin() as *const _);
452        let rid = getuid();
453        if let Ok(p) = Passwd::locate(rid) {
454            if login == p.name {
455                println!("login\t{login}");
456            }
457            println!("uid\t{}", p.name);
458        } else {
459            println!("uid\t{rid}");
460        }
461
462        let eid = getegid();
463        if eid == rid {
464            if let Ok(p) = Passwd::locate(eid) {
465                println!("euid\t{}", p.name);
466            } else {
467                println!("euid\t{eid}");
468            }
469        }
470
471        let rid = getgid();
472        if rid != eid {
473            if let Ok(g) = Group::locate(rid) {
474                println!("euid\t{}", g.name);
475            } else {
476                println!("euid\t{rid}");
477            }
478        }
479
480        println!(
481            "groups\t{}",
482            entries::get_groups_gnu(None)
483                .unwrap()
484                .iter()
485                .map(|&gr| entries::gid2grp(gr).unwrap())
486                .collect::<Vec<_>>()
487                .join(" ")
488        );
489    }
490}
491
492#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
493fn pline(possible_uid: Option<uid_t>) {
494    let uid = possible_uid.unwrap_or_else(getuid);
495    let pw = Passwd::locate(uid).unwrap();
496
497    println!(
498        "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
499        pw.name,
500        pw.user_passwd.unwrap_or_default(),
501        pw.uid,
502        pw.gid,
503        pw.user_access_class.unwrap_or_default(),
504        pw.passwd_change_time,
505        pw.expiration,
506        pw.user_info.unwrap_or_default(),
507        pw.user_dir.unwrap_or_default(),
508        pw.user_shell.unwrap_or_default()
509    );
510}
511
512#[cfg(any(target_os = "linux", target_os = "android", target_os = "openbsd"))]
513fn pline(possible_uid: Option<uid_t>) {
514    let uid = possible_uid.unwrap_or_else(getuid);
515    let pw = Passwd::locate(uid).unwrap();
516
517    println!(
518        "{}:{}:{}:{}:{}:{}:{}",
519        pw.name,
520        pw.user_passwd.unwrap_or_default(),
521        pw.uid,
522        pw.gid,
523        pw.user_info.unwrap_or_default(),
524        pw.user_dir.unwrap_or_default(),
525        pw.user_shell.unwrap_or_default()
526    );
527}
528
529#[cfg(any(target_os = "linux", target_os = "android", target_os = "openbsd"))]
530fn auditid() {}
531
532#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))]
533fn auditid() {
534    use std::mem::MaybeUninit;
535
536    let mut auditinfo: MaybeUninit<audit::c_auditinfo_addr_t> = MaybeUninit::uninit();
537    let address = auditinfo.as_mut_ptr();
538    if unsafe { audit::getaudit(address) } < 0 {
539        println!("couldn't retrieve information");
540        return;
541    }
542
543    // SAFETY: getaudit wrote a valid struct to auditinfo
544    let auditinfo = unsafe { auditinfo.assume_init() };
545
546    println!("auid={}", auditinfo.ai_auid);
547    println!("mask.success=0x{:x}", auditinfo.ai_mask.am_success);
548    println!("mask.failure=0x{:x}", auditinfo.ai_mask.am_failure);
549    println!("termid.port=0x{:x}", auditinfo.ai_termid.port);
550    println!("asid={}", auditinfo.ai_asid);
551}
552
553fn id_print(state: &State, groups: &[u32]) {
554    let uid = state.ids.as_ref().unwrap().uid;
555    let gid = state.ids.as_ref().unwrap().gid;
556    let euid = state.ids.as_ref().unwrap().euid;
557    let egid = state.ids.as_ref().unwrap().egid;
558
559    print!(
560        "uid={}({})",
561        uid,
562        entries::uid2usr(uid).unwrap_or_else(|_| {
563            show_error!("cannot find name for user ID {}", uid);
564            set_exit_code(1);
565            uid.to_string()
566        })
567    );
568    print!(
569        " gid={}({})",
570        gid,
571        entries::gid2grp(gid).unwrap_or_else(|_| {
572            show_error!("cannot find name for group ID {}", gid);
573            set_exit_code(1);
574            gid.to_string()
575        })
576    );
577    if !state.user_specified && (euid != uid) {
578        print!(
579            " euid={}({})",
580            euid,
581            entries::uid2usr(euid).unwrap_or_else(|_| {
582                show_error!("cannot find name for user ID {}", euid);
583                set_exit_code(1);
584                euid.to_string()
585            })
586        );
587    }
588    if !state.user_specified && (egid != gid) {
589        print!(
590            " egid={}({})",
591            euid,
592            entries::gid2grp(egid).unwrap_or_else(|_| {
593                show_error!("cannot find name for group ID {}", egid);
594                set_exit_code(1);
595                egid.to_string()
596            })
597        );
598    }
599    print!(
600        " groups={}",
601        groups
602            .iter()
603            .map(|&gr| format!(
604                "{}({})",
605                gr,
606                entries::gid2grp(gr).unwrap_or_else(|_| {
607                    show_error!("cannot find name for group ID {}", gr);
608                    set_exit_code(1);
609                    gr.to_string()
610                })
611            ))
612            .collect::<Vec<_>>()
613            .join(",")
614    );
615
616    #[cfg(all(any(target_os = "linux", target_os = "android"), feature = "selinux"))]
617    if state.selinux_supported
618        && !state.user_specified
619        && std::env::var_os("POSIXLY_CORRECT").is_none()
620    {
621        // print SElinux context (does not depend on "-Z")
622        if let Ok(context) = selinux::SecurityContext::current(false) {
623            let bytes = context.as_bytes();
624            print!(" context={}", String::from_utf8_lossy(bytes));
625        }
626    }
627}
628
629#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))]
630mod audit {
631    use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t};
632
633    pub type au_id_t = uid_t;
634    pub type au_asid_t = pid_t;
635    pub type au_event_t = c_uint;
636    pub type au_emod_t = c_uint;
637    pub type au_class_t = c_int;
638    pub type au_flag_t = u64;
639
640    #[repr(C)]
641    pub struct au_mask {
642        pub am_success: c_uint,
643        pub am_failure: c_uint,
644    }
645    pub type au_mask_t = au_mask;
646
647    #[repr(C)]
648    pub struct au_tid_addr {
649        pub port: dev_t,
650    }
651    pub type au_tid_addr_t = au_tid_addr;
652
653    #[repr(C)]
654    pub struct c_auditinfo_addr {
655        pub ai_auid: au_id_t,         // Audit user ID
656        pub ai_mask: au_mask_t,       // Audit masks.
657        pub ai_termid: au_tid_addr_t, // Terminal ID.
658        pub ai_asid: au_asid_t,       // Audit session ID.
659        pub ai_flags: au_flag_t,      // Audit session flags
660    }
661    pub type c_auditinfo_addr_t = c_auditinfo_addr;
662
663    extern "C" {
664        pub fn getaudit(auditinfo_addr: *mut c_auditinfo_addr_t) -> c_int;
665    }
666}