diff --git a/README.md b/README.md index 992d76e..757a553 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ It does not (yet) offer *editing* functionality; the objects returned are read-o The function `get_current_uid` returns a `uid_t` value representing the user currently running the program, and the `get_user_by_uid` function scans the users database and returns a User object with the user’s information. This function returns `None` when there is no user for that ID. -A `User` object has the following public fields: +A `User` object has the following accessors: - **uid:** The user’s ID - **name:** The user’s name @@ -42,7 +42,7 @@ Here is a complete example that prints out the current user’s name: ```rust use users::{get_user_by_uid, get_current_uid}; let user = get_user_by_uid(get_current_uid()).unwrap(); -println!("Hello, {}!", user.name); +println!("Hello, {}!", user.name()); ``` This code assumes (with `unwrap()`) that the user hasn’t been deleted after the program has started running. @@ -63,11 +63,11 @@ To introduce a cache, create a new `OSUsers` object and call the same methods on For example: ```rust -use users::{Users, OSUsers}; -let mut cache = OSUsers::empty_cache(); +use users::{Users, Groups, UsersCache}; +let mut cache = UsersCache::new(); let uid = cache.get_current_uid(); let user = cache.get_user_by_uid(uid).unwrap(); -println!("Hello again, {}!", user.name); +println!("Hello again, {}!", user.name()); ``` This cache is **only additive**: it’s not possible to drop it, or erase selected entries, as when the database may have been modified, it’s best to start entirely afresh. @@ -77,22 +77,18 @@ So to accomplish this, just start using a new `OSUsers` object. ## Groups Finally, it’s possible to get groups in a similar manner. -A `Group` object has the following public fields: +A `Group` object has the following accessors: - **gid:** The group’s ID - **name:** The group’s name -- **members:** Vector of names of the users that belong to this group And again, a complete example: ```rust -use users::{Users, OSUsers}; -let mut cache = OSUsers::empty_cache(); +use users::{Users, Groups, UsersCache}; +let mut cache = UsersCache::new(); let group = cache.get_group_by_name("admin").expect("No such group 'admin'!"); -println!("The '{}' group has the ID {}", group.name, group.gid); -for member in group.members.into_iter() { - println!("{} is a member of the group", member); -} +println!("The '{}' group has the ID {}", group.name(), group.gid()); ``` @@ -117,19 +113,13 @@ Aside from that, you can add users and groups with `add_user` and `add_group` to ```rust use users::mock::{MockUsers, User, Group}; +use users::os::unix::{UserExt, GroupExt}; +use std::sync::Arc; + let mut users = MockUsers::with_current_uid(1000); -users.add_user(User { - uid: 1000, - name: "Bobbins".to_string(), - primary_group: 100, - home_dir: "/home/bobbins".to_string(), - shell: "/bin/bash".to_string(), -}); -users.add_group(Group { - gid: 100, - name: "funkyppl".to_string(), - members: vec![ "other_person".to_string() ] -}); +let bobbins = User::new(1000, "Bobbins", 1000).with_home_dir("/home/bobbins"); +users.add_user(bobbins); +users.add_group(Group::new(100, "funkyppl")); ``` The exports get re-exported into the mock module, for simpler `use` lines. @@ -143,24 +133,19 @@ Then, you can pass in an object of either OS or Mock type. Here's a complete example: ```rust -use users::{Users, OSUsers, User}; +use users::{Users, UsersCache, User}; +use users::os::unix::UserExt; use users::mock::MockUsers; +use std::sync::Arc; fn print_current_username(users: &mut U) { println!("Current user: {:?}", users.get_current_username()); } let mut users = MockUsers::with_current_uid(1001); -users.add_user(User { - uid: 1001, - name: "fred".to_string(), - primary_group: 101, - home_dir: "/home/fred".to_string(), - shell: "/bin/bash".to_string(), -}); - +users.add_user(User::new(1001, "fred", 101)); print_current_username(&mut users); -let mut actual_users = OSUsers::empty_cache(); +let mut actual_users = UsersCache::new(); print_current_username(&mut actual_users); ``` diff --git a/examples/example.rs b/examples/example.rs index c2f34a5..2df0cf5 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -8,17 +8,8 @@ fn main() { println!("Your UID is {}", current_uid); let you = cache.get_user_by_uid(current_uid).expect("No entry for current user!"); - println!("Your username is {}", you.name); + println!("Your username is {}", you.name()); - let primary_group = cache.get_group_by_gid(you.primary_group).expect("No entry for your primary group!"); - println!("Your primary group has ID {} and name {}", primary_group.gid, primary_group.name); - - if primary_group.members.is_empty() { - println!("There are no other members of that group."); - } - else { - for username in primary_group.members.iter() { - println!("User {} is also a member of that group.", username); - } - } + let primary_group = cache.get_group_by_gid(you.primary_group_id()).expect("No entry for your primary group!"); + println!("Your primary group has ID {} and name {}", primary_group.gid(), primary_group.name()); } diff --git a/examples/os.rs b/examples/os.rs new file mode 100644 index 0000000..5829625 --- /dev/null +++ b/examples/os.rs @@ -0,0 +1,33 @@ +extern crate users; +use users::{Users, Groups, UsersCache}; +use users::os::unix::{UserExt, GroupExt}; +//use users::os::bsd::UserExt as BSDUserExt; + +fn main() { + let cache = UsersCache::new(); + + let current_uid = cache.get_current_uid(); + println!("Your UID is {}", current_uid); + + let you = cache.get_user_by_uid(current_uid).expect("No entry for current user!"); + println!("Your username is {}", you.name()); + println!("Your shell is {}", you.shell().display()); + println!("Your home directory is {}", you.home_dir().display()); + + // The two fields below are only available on BSD systems. + // Linux systems don’t have the fields in their `passwd` structs! + //println!("Your password change timestamp is {}", you.password_change_time()); + //println!("Your password expiry timestamp is {}", you.password_expire_time()); + + let primary_group = cache.get_group_by_gid(you.primary_group_id()).expect("No entry for your primary group!"); + println!("Your primary group has ID {} and name {}", primary_group.gid(), primary_group.name()); + + if primary_group.members().is_empty() { + println!("There are no other members of that group."); + } + else { + for username in primary_group.members() { + println!("User {} is also a member of that group.", username); + } + } +} diff --git a/examples/threading.rs b/examples/threading.rs index 27ffa54..a518f4e 100644 --- a/examples/threading.rs +++ b/examples/threading.rs @@ -52,7 +52,7 @@ fn main() { for uid in LO .. HI { let cache = cache.lock().unwrap(); // Re-unlock the mutex if let Some(u) = cache.get_user_by_uid(uid) { // Re-query our cache! - println!("User #{} is {}", u.uid, u.name) + println!("User #{} is {}", u.uid(), u.name()) } else { println!("User #{} does not exist", uid); diff --git a/src/base.rs b/src/base.rs index dd643a4..11650fd 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,6 +1,36 @@ +//! Integration with the C library’s users and groups. +//! +//! This module uses `extern` functions and types from `libc` that integrate +//! with the system’s C library, which integrates with the OS itself to get user +//! and group information. It’s where the “core” user handling is done. +//! +//! +//! ## Name encoding rules +//! +//! Under Unix, usernames and group names are considered to be +//! null-terminated, UTF-8 strings. These are `CString`s in Rust, although in +//! this library, they are just `String` values. Why? +//! +//! The reason is that any user or group values with invalid `CString` data +//! can instead just be assumed to not exist: +//! +//! - If you try to search for a user with a null character in their name, +//! such a user could not exist anyway—so it’s OK to return `None`. +//! - If the OS returns user information with a null character in a field, +//! then that field will just be truncated instead, which is valid behaviour +//! for a `CString`. +//! +//! The downside is that we use `from_utf8_lossy` instead, which has a small +//! runtime penalty when it calculates and scans the length of the string for +//! invalid characters. However, this should not be a problem when dealing with +//! usernames of a few bytes each. +//! +//! In short, if you want to check for null characters in user fields, your +//! best bet is to check for them yourself before passing strings into any +//! functions. + use std::ffi::{CStr, CString}; use std::ptr::read; -use std::str::from_utf8_unchecked; use std::sync::Arc; use libc::{uid_t, gid_t}; @@ -11,10 +41,12 @@ use libc::{c_char, time_t}; #[cfg(target_os = "linux")] use libc::c_char; +//use os::*; + #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] #[repr(C)] -struct c_passwd { +pub struct c_passwd { pw_name: *const c_char, // user name pw_passwd: *const c_char, // password field pw_uid: uid_t, // user ID @@ -29,7 +61,7 @@ struct c_passwd { #[cfg(target_os = "linux")] #[repr(C)] -struct c_passwd { +pub struct c_passwd { pw_name: *const c_char, // user name pw_passwd: *const c_char, // password field pw_uid: uid_t, // user ID @@ -40,7 +72,7 @@ struct c_passwd { } #[repr(C)] -struct c_group { +pub struct c_group { gr_name: *const c_char, // group name gr_passwd: *const c_char, // password gr_gid: gid_t, // group id @@ -64,50 +96,118 @@ extern { /// Information about a particular user. #[derive(Clone)] pub struct User { + uid: uid_t, + primary_group: gid_t, + extras: os::UserExtras, - /// This user's ID - pub uid: uid_t, + /// This user’s name, as an owned `String` possibly shared with a cache. + /// Prefer using the `name()` accessor to using this field, if possible. + pub name_arc: Arc, +} - /// This user's name - pub name: Arc, +impl User { - /// The ID of this user's primary group - pub primary_group: gid_t, + /// Create a new `User` with the given user ID, name, and primary + /// group ID, with the rest of the fields filled with dummy values. + /// + /// This method does not actually create a new user on the system—it + /// should only be used for comparing users in tests. + pub fn new(uid: uid_t, name: &str, primary_group: gid_t) -> User { + User { + uid: uid, + name_arc: Arc::new(name.to_owned()), + primary_group: primary_group, + extras: os::UserExtras::default(), + } + } - /// This user's home directory - pub home_dir: String, + /// Returns this user’s ID. + pub fn uid(&self) -> uid_t { + self.uid.clone() + } - /// This user's shell - pub shell: String, + /// Returns this user’s name. + pub fn name(&self) -> &str { + &**self.name_arc + } + + /// Returns the ID of this user’s primary group. + pub fn primary_group_id(&self) -> gid_t { + self.primary_group.clone() + } } /// Information about a particular group. #[derive(Clone)] pub struct Group { + gid: gid_t, + extras: os::GroupExtras, - /// This group's ID - pub gid: uid_t, - - /// This group's name - pub name: Arc, - - /// Vector of the names of the users who belong to this group as a non-primary member - pub members: Vec, + /// This group’s name, as an owned `String` possibly shared with a cache. + /// Prefer using the `name()` accessor to using this field, if possible. + pub name_arc: Arc, } -unsafe fn from_raw_buf(p: *const i8) -> String { - from_utf8_unchecked(CStr::from_ptr(p).to_bytes()).to_string() +impl Group { + + /// Create a new `Group` with the given group ID and name, with the + /// rest of the fields filled in with dummy values. + /// + /// This method does not actually create a new group on the system—it + /// should only be used for comparing groups in tests. + pub fn new(gid: gid_t, name: &str) -> Self { + Group { + gid: gid, + name_arc: Arc::new(String::from(name)), + extras: os::GroupExtras::default(), + } + } + + /// Returns this group’s ID. + pub fn gid(&self) -> gid_t { + self.gid.clone() + } + + /// Returns this group's name. + pub fn name(&self) -> &str { + &**self.name_arc + } +} + +/// Reads data from a `*char` field in `c_passwd` or `g_group` into a UTF-8 +/// `String` for use in a user or group value. +/// +/// Although `from_utf8_lossy` returns a clone-on-write string, we immediately +/// clone it anyway: the underlying buffer is managed by the C library, not by +/// us, so we *need* to move data out of it before the next user gets read. +unsafe fn from_raw_buf(p: *const c_char) -> String { + CStr::from_ptr(p).to_string_lossy().into_owned() +} + +/// Converts a raw pointer, which could be null, into a safe reference that +/// might be `None` instead. +/// +/// This is basically the unstable `ptr_as_ref` feature: +/// https://github.com/rust-lang/rust/issues/27780 +/// When that stabilises, this can be replaced. +unsafe fn ptr_as_ref(pointer: *const T) -> Option { + if pointer.is_null() { + None + } + else { + Some(read(pointer)) + } } unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { - if !pointer.is_null() { - let pw = read(pointer); + if let Some(passwd) = ptr_as_ref(pointer) { + let name = Arc::new(from_raw_buf(passwd.pw_name)); + Some(User { - uid: pw.pw_uid as uid_t, - name: Arc::new(from_raw_buf(pw.pw_name as *const i8)), - primary_group: pw.pw_gid as gid_t, - home_dir: from_raw_buf(pw.pw_dir as *const i8), - shell: from_raw_buf(pw.pw_shell as *const i8) + uid: passwd.pw_uid, + name_arc: name, + primary_group: passwd.pw_gid, + extras: os::UserExtras::from_passwd(passwd), }) } else { @@ -116,76 +216,95 @@ unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { } unsafe fn struct_to_group(pointer: *const c_group) -> Option { - if !pointer.is_null() { - let gr = read(pointer); - let name = from_raw_buf(gr.gr_name as *const i8); - let members = members(gr.gr_mem); - Some(Group { gid: gr.gr_gid, name: Arc::new(name), members: members }) + if let Some(group) = ptr_as_ref(pointer) { + let name = Arc::new(from_raw_buf(group.gr_name)); + + Some(Group { + gid: group.gr_gid, + name_arc: name, + extras: os::GroupExtras::from_struct(group), + }) } else { None } } +/// Expand a list of group members to a vector of strings. +/// +/// The list of members is, in true C fashion, a pointer to a pointer of +/// characters, terminated by a null pointer. We check `members[0]`, then +/// `members[1]`, and so on, until that null pointer is reached. It doesn't +/// specify whether we should expect a null pointer or a pointer to a null +/// pointer, so we check for both here! unsafe fn members(groups: *const *const c_char) -> Vec { - let mut i = 0; - let mut members = vec![]; + let mut members = Vec::new(); - // The list of members is a pointer to a pointer of characters, terminated - // by a null pointer. - loop { + for i in 0.. { let username = groups.offset(i); - // The first null check here should be unnecessary, but if libc sends - // us bad data, it's probably better to continue on than crashing... if username.is_null() || (*username).is_null() { - return members; + break; + } + else { + members.push(from_raw_buf(*username)); } - - members.push(from_raw_buf(*username)); - i += 1; } + + members } /// Searches for a `User` with the given ID in the system’s user database. /// Returns it if one is found, otherwise returns `None`. pub fn get_user_by_uid(uid: uid_t) -> Option { - unsafe { passwd_to_user(getpwuid(uid)) } + unsafe { + let passwd = getpwuid(uid); + passwd_to_user(passwd) + } } /// Searches for a `User` with the given username in the system’s user database. /// Returns it if one is found, otherwise returns `None`. pub fn get_user_by_name(username: &str) -> Option { - let username_c = CString::new(username); - - if !username_c.is_ok() { - // This usually means the given username contained a '\0' already - // It is debatable what to do here - return None; + if let Ok(username) = CString::new(username) { + unsafe { + let passwd = getpwnam(username.as_ptr()); + passwd_to_user(passwd) + } + } + else { + // The username that was passed in contained a null character. + // This will *never* find anything, so just return `None`. + // (I can’t figure out a pleasant way to signal an error here) + None } - - unsafe { passwd_to_user(getpwnam(username_c.unwrap().as_ptr())) } } /// Searches for a `Group` with the given ID in the system’s group database. /// Returns it if one is found, otherwise returns `None`. pub fn get_group_by_gid(gid: gid_t) -> Option { - unsafe { struct_to_group(getgrgid(gid)) } + unsafe { + let group = getgrgid(gid); + struct_to_group(group) + } } -/// Searches for a `Group` with the given group name in the system‘s group database. +/// Searches for a `Group` with the given group name in the system’s group database. /// Returns it if one is found, otherwise returns `None`. pub fn get_group_by_name(group_name: &str) -> Option { - let group_name_c = CString::new(group_name); - - if !group_name_c.is_ok() { - // This usually means the given username contained a '\0' already - // It is debatable what to do here - return None; + if let Ok(group_name) = CString::new(group_name) { + unsafe { + let group = getgrnam(group_name.as_ptr()); + struct_to_group(group) + } + } + else { + // The group name that was passed in contained a null character. + // This will *never* find anything, so just return `None`. + // (I can’t figure out a pleasant way to signal an error here) + None } - - unsafe { struct_to_group(getgrnam(group_name_c.unwrap().as_ptr())) } } /// Returns the user ID for the user running the process. @@ -196,7 +315,7 @@ pub fn get_current_uid() -> uid_t { /// Returns the username of the user running the process. pub fn get_current_username() -> Option { let uid = get_current_uid(); - get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name).unwrap()) + get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name_arc).unwrap()) } /// Returns the user ID for the effective user running the process. @@ -207,7 +326,7 @@ pub fn get_effective_uid() -> uid_t { /// Returns the username of the effective user running the process. pub fn get_effective_username() -> Option { let uid = get_effective_uid(); - get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name).unwrap()) + get_user_by_uid(uid).map(|u| Arc::try_unwrap(u.name_arc).unwrap()) } /// Returns the group ID for the user running the process. @@ -218,7 +337,7 @@ pub fn get_current_gid() -> gid_t { /// Returns the groupname of the user running the process. pub fn get_current_groupname() -> Option { let gid = get_current_gid(); - get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name).unwrap()) + get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name_arc).unwrap()) } /// Returns the group ID for the effective user running the process. @@ -229,7 +348,255 @@ pub fn get_effective_gid() -> gid_t { /// Returns the groupname of the effective user running the process. pub fn get_effective_groupname() -> Option { let gid = get_effective_gid(); - get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name).unwrap()) + get_group_by_gid(gid).map(|g| Arc::try_unwrap(g.name_arc).unwrap()) +} + + + + +/// OS-specific extensions to users and groups. +/// +/// Every OS has a different idea of what data a user or a group comes with. +/// Although they all provide a *username*, some OS’ users have an *actual name* +/// too, or a set of permissions or directories or timestamps associated with +/// them. +/// +/// This module provides extension traits for users and groups that allow +/// implementors of this library to access this data *as long as a trait is +/// available*, which requires the OS they’re using to support this data. +/// +/// It’s the same method taken by `Metadata` in the standard Rust library, +/// which has a few cross-platform fields and many more OS-specific fields: +/// traits in `std::os` provides access to any data that is not guaranteed to +/// be there in the actual struct. +pub mod os { + + /// Extensions to users and groups for Unix platforms. + /// + /// Although the `passwd` struct is common among Unix systems, its actual + /// format can vary. See the definitions in the `base` module to check which + /// fields are actually present. + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub mod unix { + use std::path::Path; + + use super::super::{c_passwd, c_group, members, from_raw_buf, Group}; + + /// Unix-specific extensions for `User`s. + pub trait UserExt { + + /// Returns a path to this user’s home directory. + fn home_dir(&self) -> &Path; + + /// Sets this user value’s home directory to the given string. + /// Can be used to construct test users, which by default come with a + /// dummy home directory string. + fn with_home_dir(mut self, home_dir: &str) -> Self; + + /// Returns a path to this user’s shell. + fn shell(&self) -> &Path; + + /// Sets this user’s shell path to the given string. + /// Can be used to construct test users, which by default come with a + /// dummy shell field. + fn with_shell(mut self, shell: &str) -> Self; + + // TODO(ogham): Isn’t it weird that the setters take string slices, but + // the getters return paths? + } + + /// Unix-specific extensions for `Group`s. + pub trait GroupExt { + + /// Returns a slice of the list of users that are in this group as + /// their non-primary group. + fn members(&self) -> &[String]; + } + + /// Unix-specific fields for `User`s. + #[derive(Clone)] + pub struct UserExtras { + + /// The path to the user’s home directory. + pub home_dir: String, + + /// The path to the user’s shell. + pub shell: String, + } + + impl Default for UserExtras { + fn default() -> UserExtras { + UserExtras { + home_dir: String::from("/var/empty"), + shell: String::from("/bin/false"), + } + } + } + + impl UserExtras { + /// Extract the OS-specific fields from the C `passwd` struct that + /// we just read. + pub unsafe fn from_passwd(passwd: c_passwd) -> UserExtras { + let home_dir = from_raw_buf(passwd.pw_dir); + let shell = from_raw_buf(passwd.pw_shell); + + UserExtras { + home_dir: home_dir, + shell: shell, + } + } + } + + #[cfg(any(target_os = "linux"))] + use super::super::User; + + #[cfg(any(target_os = "linux"))] + impl UserExt for User { + fn home_dir(&self) -> &Path { + Path::new(&self.extras.home_dir) + } + + fn with_home_dir(mut self, home_dir: &str) -> User { + self.extras.home_dir = home_dir.to_owned(); + self + } + + fn shell(&self) -> &Path { + Path::new(&self.extras.shell) + } + + fn with_shell(mut self, shell: &str) -> User { + self.extras.shell = shell.to_owned(); + self + } + } + + /// Unix-specific fields for `Group`s. + #[derive(Clone, Default)] + pub struct GroupExtras { + + /// Vector of usernames that are members of this group. + pub members: Vec, + } + + impl GroupExtras { + /// Extract the OS-specific fields from the C `group` struct that + /// we just read. + pub unsafe fn from_struct(group: c_group) -> GroupExtras { + let members = members(group.gr_mem); + + GroupExtras { + members: members, + } + } + } + + impl GroupExt for Group { + fn members(&self) -> &[String] { + &*self.extras.members + } + } + } + + /// Extensions to users and groups for BSD platforms. + /// + /// These platforms have `change` and `expire` fields in their `passwd` + /// C structs. + #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub mod bsd { + use std::path::Path; + use libc::time_t; + use super::super::{c_passwd, User}; + + /// BSD-specific fields for `User`s. + #[derive(Clone)] + pub struct UserExtras { + + /// Fields specific to Unix, rather than just BSD. (This struct is + /// a superset, so it has to have all the other fields in it, too). + pub extras: super::unix::UserExtras, + + /// Password change time. + pub change: time_t, + + /// Password expiry time. + pub expire: time_t, + } + + impl UserExtras { + /// Extract the OS-specific fields from the C `passwd` struct that + /// we just read. + pub unsafe fn from_passwd(passwd: c_passwd) -> UserExtras { + UserExtras { + change: passwd.pw_change, + expire: passwd.pw_expire, + extras: super::unix::UserExtras::from_passwd(passwd), + } + } + } + + impl super::unix::UserExt for User { + fn home_dir(&self) -> &Path { + Path::new(&self.extras.extras.home_dir) + } + + fn with_home_dir(mut self, home_dir: &str) -> User { + self.extras.extras.home_dir = home_dir.to_owned(); + self + } + + fn shell(&self) -> &Path { + Path::new(&self.extras.extras.shell) + } + + fn with_shell(mut self, shell: &str) -> User { + self.extras.extras.shell = shell.to_owned(); + self + } + } + + /// BSD-specific accessors for `User`s. + pub trait UserExt { + + /// Returns this user's password change timestamp. + fn password_change_time(&self) -> time_t; + + /// Returns this user's password expiry timestamp. + fn password_expire_time(&self) -> time_t; + } + + impl UserExt for User { + fn password_change_time(&self) -> time_t { + self.extras.change.clone() + } + + fn password_expire_time(&self) -> time_t { + self.extras.expire.clone() + } + } + + impl Default for UserExtras { + fn default() -> UserExtras { + UserExtras { + extras: super::unix::UserExtras::default(), + change: 0, + expire: 0, + } + } + } + } + + /// Any extra fields on a `User` specific to the current platform. + #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub type UserExtras = bsd::UserExtras; + + /// Any extra fields on a `User` specific to the current platform. + #[cfg(any(target_os = "linux"))] + pub type UserExtras = unix::UserExtras; + + /// Any extra fields on a `Group` specific to the current platform. + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub type GroupExtras = unix::GroupExtras; } @@ -245,7 +612,7 @@ mod test { #[test] fn username() { let uid = get_current_uid(); - assert_eq!(&*get_current_username().unwrap(), &*get_user_by_uid(uid).unwrap().name); + assert_eq!(&*get_current_username().unwrap(), &*get_user_by_uid(uid).unwrap().name()); } #[test] @@ -265,11 +632,13 @@ mod test { #[test] fn user_info() { + use base::os::unix::UserExt; + let uid = get_current_uid(); let user = get_user_by_uid(uid).unwrap(); // Not a real test but can be used to verify correct results // Use with --nocapture on test executable to show output - println!("HOME={}, SHELL={}", user.home_dir, user.shell); + println!("HOME={:?}, SHELL={:?}", user.home_dir(), user.shell()); } #[test] @@ -279,7 +648,7 @@ mod test { let name = get_current_username().unwrap(); let user_by_name = get_user_by_name(&name); assert!(user_by_name.is_some()); - assert_eq!(&**user_by_name.unwrap().name, &*name); + assert_eq!(user_by_name.unwrap().name(), &*name); // User names containing '\0' cannot be used (for now) let user = get_user_by_name("user\0"); @@ -293,10 +662,10 @@ mod test { let cur_uid = get_current_uid(); let cur_user = get_user_by_uid(cur_uid).unwrap(); let cur_group = get_group_by_gid(cur_user.primary_group).unwrap(); - let group_by_name = get_group_by_name(&cur_group.name); + let group_by_name = get_group_by_name(&cur_group.name()); assert!(group_by_name.is_some()); - assert_eq!(group_by_name.unwrap().name, cur_group.name); + assert_eq!(group_by_name.unwrap().name(), cur_group.name()); // Group names containing '\0' cannot be used (for now) let group = get_group_by_name("users\0"); diff --git a/src/cache.rs b/src/cache.rs index bd96834..54fbd34 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -134,7 +134,7 @@ impl Users for UsersCache { Vacant(entry) => { match super::get_user_by_uid(uid) { Some(user) => { - let newsername = user.name.clone(); + let newsername = user.name_arc.clone(); let mut users_backward = self.users.backward.borrow_mut(); users_backward.insert(newsername, Some(uid)); @@ -161,7 +161,7 @@ impl Users for UsersCache { Vacant(entry) => { match super::get_user_by_name(username) { Some(user) => { - let uid = user.uid; + let uid = user.uid(); let user_arc = Arc::new(user); let mut users_forward = self.users.forward.borrow_mut(); @@ -199,7 +199,7 @@ impl Users for UsersCache { fn get_current_username(&self) -> Option> { let uid = self.get_current_uid(); - self.get_user_by_uid(uid).map(|u| u.name.clone()) + self.get_user_by_uid(uid).map(|u| u.name_arc.clone()) } fn get_effective_uid(&self) -> uid_t { @@ -215,7 +215,7 @@ impl Users for UsersCache { fn get_effective_username(&self) -> Option> { let uid = self.get_effective_uid(); - self.get_user_by_uid(uid).map(|u| u.name.clone()) + self.get_user_by_uid(uid).map(|u| u.name_arc.clone()) } } @@ -228,7 +228,7 @@ impl Groups for UsersCache { let group = super::get_group_by_gid(gid); match group { Some(group) => { - let new_group_name = group.name.clone(); + let new_group_name = group.name_arc.clone(); let mut groups_backward = self.groups.backward.borrow_mut(); groups_backward.insert(new_group_name, Some(gid)); @@ -257,7 +257,7 @@ impl Groups for UsersCache { match user { Some(group) => { let group_arc = Arc::new(group.clone()); - let gid = group.gid; + let gid = group.gid(); let mut groups_forward = self.groups.forward.borrow_mut(); groups_forward.insert(gid, Some(group_arc.clone())); @@ -294,7 +294,7 @@ impl Groups for UsersCache { fn get_current_groupname(&self) -> Option> { let gid = self.get_current_gid(); - self.get_group_by_gid(gid).map(|g| g.name.clone()) + self.get_group_by_gid(gid).map(|g| g.name_arc.clone()) } fn get_effective_gid(&self) -> gid_t { @@ -310,6 +310,6 @@ impl Groups for UsersCache { fn get_effective_groupname(&self) -> Option> { let gid = self.get_effective_gid(); - self.get_group_by_gid(gid).map(|g| g.name.clone()) + self.get_group_by_gid(gid).map(|g| g.name_arc.clone()) } } diff --git a/src/lib.rs b/src/lib.rs index a3f185e..8df22d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ //! users database and returns a User object with the user’s information. This //! function returns `None` when there is no user for that ID. //! -//! A `User` object has the following public fields: +//! A `User` object has the following accessors: //! //! - **uid:** The user’s ID //! - **name:** The user’s name @@ -35,7 +35,7 @@ //! ```rust //! use users::{get_user_by_uid, get_current_uid}; //! let user = get_user_by_uid(get_current_uid()).unwrap(); -//! println!("Hello, {}!", user.name); +//! println!("Hello, {}!", user.name()); //! ``` //! //! This code assumes (with `unwrap()`) that the user hasn’t been deleted after @@ -68,7 +68,7 @@ //! let mut cache = UsersCache::new(); //! let uid = cache.get_current_uid(); //! let user = cache.get_user_by_uid(uid).unwrap(); -//! println!("Hello again, {}!", user.name); +//! println!("Hello again, {}!", user.name()); //! ``` //! //! This cache is **only additive**: it’s not possible to drop it, or erase @@ -80,22 +80,18 @@ //! ## Groups //! //! Finally, it’s possible to get groups in a similar manner. -//! A `Group` has the following public fields: +//! A `Group` has the following accessors: //! //! - **gid:** The group’s ID //! - **name:** The group’s name -//! - **members:** Vector of names of the users that belong to this group //! //! And again, a complete example: //! -//! ```rust +//! ```no_run //! use users::{Users, Groups, UsersCache}; //! let mut cache = UsersCache::new(); //! let group = cache.get_group_by_name("admin").expect("No such group 'admin'!"); -//! println!("The '{}' group has the ID {}", group.name, group.gid); -//! for member in &group.members { -//! println!("{} is a member of the group", member); -//! } +//! println!("The '{}' group has the ID {}", group.name(), group.gid()); //! ``` //! //! @@ -108,11 +104,23 @@ //! Use the mocking module to create custom tables to test your code for these //! edge cases. +#![warn(missing_copy_implementations)] +#![warn(missing_docs)] +#![warn(trivial_casts, trivial_numeric_casts)] +#![warn(unused_extern_crates, unused_qualifications)] + extern crate libc; pub use libc::{uid_t, gid_t}; mod base; -pub use base::*; +pub use base::{User, Group, os}; +pub use base::{get_user_by_uid, get_user_by_name}; +pub use base::{get_group_by_gid, get_group_by_name}; +pub use base::{get_current_uid, get_current_username}; +pub use base::{get_effective_uid, get_effective_username}; +pub use base::{get_current_gid, get_current_groupname}; +pub use base::{get_effective_gid, get_effective_groupname}; + pub mod cache; pub use cache::UsersCache; diff --git a/src/mock.rs b/src/mock.rs index 65c7c0d..3712861 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -18,11 +18,13 @@ //! //! ```rust //! use users::mock::{MockUsers, User, Group}; +//! use users::os::unix::{UserExt, GroupExt}; //! use std::sync::Arc; //! //! let mut users = MockUsers::with_current_uid(1000); -//! users.add_user(User { uid: 1000, name: Arc::new("Bobbins".to_string()), primary_group: 100, home_dir: "/home/bobbins".to_string(), shell: "/bin/bash".to_string() }); -//! users.add_group(Group { gid: 100, name: Arc::new("funkyppl".to_string()), members: vec![ "other_person".to_string() ] }); +//! let bobbins = User::new(1000, "Bobbins", 1000).with_home_dir("/home/bobbins"); +//! users.add_user(bobbins); +//! users.add_group(Group::new(100, "funkyppl")); //! ``` //! //! The exports get re-exported into the mock module, for simpler `use` lines. @@ -38,6 +40,7 @@ //! //! ```rust //! use users::{Users, UsersCache, User}; +//! use users::os::unix::UserExt; //! use users::mock::MockUsers; //! use std::sync::Arc; //! @@ -46,7 +49,7 @@ //! } //! //! let mut users = MockUsers::with_current_uid(1001); -//! users.add_user(User { uid: 1001, name: Arc::new("fred".to_string()), primary_group: 101 , home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string()}); +//! users.add_user(User::new(1001, "fred", 101)); //! print_current_username(&mut users); //! //! let mut actual_users = UsersCache::new(); @@ -81,12 +84,12 @@ impl MockUsers { /// Add a user to the users table. pub fn add_user(&mut self, user: User) -> Option> { - self.users.insert(user.uid, Arc::new(user)) + self.users.insert(user.uid(), Arc::new(user)) } /// Add a group to the groups table. pub fn add_group(&mut self, group: Group) -> Option> { - self.groups.insert(group.gid, Arc::new(group)) + self.groups.insert(group.gid(), Arc::new(group)) } } @@ -96,7 +99,7 @@ impl Users for MockUsers { } fn get_user_by_name(&self, username: &str) -> Option> { - self.users.values().find(|u| &*u.name == username).cloned() + self.users.values().find(|u| u.name() == username).cloned() } fn get_current_uid(&self) -> uid_t { @@ -104,7 +107,7 @@ impl Users for MockUsers { } fn get_current_username(&self) -> Option> { - self.users.get(&self.uid).map(|u| u.name.clone()) + self.users.get(&self.uid).map(|u| u.name_arc.clone()) } fn get_effective_uid(&self) -> uid_t { @@ -112,7 +115,7 @@ impl Users for MockUsers { } fn get_effective_username(&self) -> Option> { - self.users.get(&self.uid).map(|u| u.name.clone()) + self.users.get(&self.uid).map(|u| u.name_arc.clone()) } } @@ -122,7 +125,7 @@ impl Groups for MockUsers { } fn get_group_by_name(&self, group_name: &str) -> Option> { - self.groups.values().find(|g| &*g.name == group_name).cloned() + self.groups.values().find(|g| g.name() == group_name).cloned() } fn get_current_gid(&self) -> uid_t { @@ -130,7 +133,7 @@ impl Groups for MockUsers { } fn get_current_groupname(&self) -> Option> { - self.groups.get(&self.uid).map(|u| u.name.clone()) + self.groups.get(&self.uid).map(|u| u.name_arc.clone()) } fn get_effective_gid(&self) -> uid_t { @@ -138,13 +141,13 @@ impl Groups for MockUsers { } fn get_effective_groupname(&self) -> Option> { - self.groups.get(&self.uid).map(|u| u.name.clone()) + self.groups.get(&self.uid).map(|u| u.name_arc.clone()) } } #[cfg(test)] mod test { - use super::{MockUsers}; + use super::MockUsers; use base::{User, Group}; use traits::{Users, Groups}; use std::sync::Arc; @@ -152,7 +155,7 @@ mod test { #[test] fn current_username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1337, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); + users.add_user(User::new(1337, "fred", 101)); assert_eq!(Some(Arc::new("fred".into())), users.get_current_username()) } @@ -165,54 +168,54 @@ mod test { #[test] fn uid() { let mut users = MockUsers::with_current_uid(0); - users.add_user(User { uid: 1337, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); - assert_eq!(Some(Arc::new("fred".into())), users.get_user_by_uid(1337).map(|u| u.name.clone())) + users.add_user(User::new(1337, "fred", 101)); + assert_eq!(Some(Arc::new("fred".into())), users.get_user_by_uid(1337).map(|u| u.name_arc.clone())) } #[test] fn username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1440, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); - assert_eq!(Some(1440), users.get_user_by_name("fred").map(|u| u.uid)) + users.add_user(User::new(1440, "fred", 101)); + assert_eq!(Some(1440), users.get_user_by_name("fred").map(|u| u.uid())) } #[test] fn no_username() { let mut users = MockUsers::with_current_uid(1337); - users.add_user(User { uid: 1440, name: Arc::new("fred".to_string()), primary_group: 101, home_dir: "/home/fred".to_string(), shell: "/bin/bash".to_string() }); - assert_eq!(None, users.get_user_by_name("criminy").map(|u| u.uid)) + users.add_user(User::new(1337, "fred", 101)); + assert_eq!(None, users.get_user_by_name("criminy").map(|u| u.uid())) } #[test] fn no_uid() { let users = MockUsers::with_current_uid(0); - assert_eq!(None, users.get_user_by_uid(1337).map(|u| u.name.clone())) + assert_eq!(None, users.get_user_by_uid(1337).map(|u| u.name_arc.clone())) } #[test] fn gid() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); - assert_eq!(Some(Arc::new("fred".into())), users.get_group_by_gid(1337).map(|g| g.name.clone())) + users.add_group(Group::new(1337, "fred")); + assert_eq!(Some(Arc::new("fred".into())), users.get_group_by_gid(1337).map(|g| g.name_arc.clone())) } #[test] fn group_name() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); - assert_eq!(Some(1337), users.get_group_by_name("fred").map(|g| g.gid)) + users.add_group(Group::new(1337, "fred")); + assert_eq!(Some(1337), users.get_group_by_name("fred").map(|g| g.gid())) } #[test] fn no_group_name() { let mut users = MockUsers::with_current_uid(0); - users.add_group(Group { gid: 1337, name: Arc::new("fred".to_string()), members: vec![], }); - assert_eq!(None, users.get_group_by_name("santa").map(|g| g.gid)) + users.add_group(Group::new(1337, "fred")); + assert_eq!(None, users.get_group_by_name("santa").map(|g| g.gid())) } #[test] fn no_gid() { let users = MockUsers::with_current_uid(0); - assert_eq!(None, users.get_group_by_gid(1337).map(|g| g.name.clone())) + assert_eq!(None, users.get_group_by_gid(1337).map(|g| g.name_arc.clone())) } } diff --git a/src/switch.rs b/src/switch.rs index 4cceac0..a9ceece 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -1,3 +1,5 @@ +//! Functions for switching the running process’s user or group. + use std::io::{Error as IOError, Result as IOResult}; use libc::{uid_t, gid_t, c_int}; @@ -16,60 +18,86 @@ extern { } -/// Sets current user for the running process, requires root priviledges. +/// Sets the **current user** for the running process to the one with the +/// given user ID. Uses `setuid` internally. +/// +/// Typically, trying to switch to anyone other than the user already running +/// the process requires root privileges. pub fn set_current_uid(uid: uid_t) -> IOResult<()> { match unsafe { setuid(uid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("setuid returned {}", n) } } -/// Set current group for the running process, requires root priviledges. +/// Sets the **current group** for the running process to the one with the +/// given group ID. Uses `setgid` internally. +/// +/// Typically, trying to switch to any group other than the group already +/// running the process requires root privileges. pub fn set_current_gid(gid: gid_t) -> IOResult<()> { match unsafe { setgid(gid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("setgid returned {}", n) } } -/// Set effective user for the running process, requires root priviledges. +/// Sets the **effective user** for the running process to the one with the +/// given user ID. Uses `seteuid` internally. +/// +/// Typically, trying to switch to anyone other than the user already running +/// the process requires root privileges. pub fn set_effective_uid(uid: uid_t) -> IOResult<()> { match unsafe { seteuid(uid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("seteuid returned {}", n) } } -/// Set effective user for the running process, requires root priviledges. +/// Sets the **effective group** for the running process to the one with the +/// given group ID. Uses `setegid` internally. +/// +/// Typically, trying to switch to any group other than the group already +/// running the process requires root privileges. pub fn set_effective_gid(gid: gid_t) -> IOResult<()> { match unsafe { setegid(gid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("setegid returned {}", n) } } -/// Atomically set current and effective user for the running process, requires root priviledges. +/// Sets both the **current user** and the **effective user** for the running +/// process to the ones with the given user IDs. Uses `setreuid` internally. +/// +/// Typically, trying to switch to anyone other than the user already running +/// the process requires root privileges. pub fn set_both_uid(ruid: uid_t, euid: uid_t) -> IOResult<()> { match unsafe { setreuid(ruid, euid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("setreuid returned {}", n) } } -/// Atomically set current and effective group for the running process, requires root priviledges. +/// Sets both the **current group** and the **effective group** for the +/// running process to the ones with the given group IDs. Uses `setregid` +/// internally. +/// +/// Typically, trying to switch to any group other than the group already +/// running the process requires root privileges. pub fn set_both_gid(rgid: gid_t, egid: gid_t) -> IOResult<()> { match unsafe { setregid(rgid, egid) } { - 0 => Ok(()), + 0 => Ok(()), -1 => Err(IOError::last_os_error()), - _ => unreachable!() + n => unreachable!("setregid returned {}", n) } } +/// Guard returned from a `switch_user_group` call. pub struct SwitchUserGuard { uid: uid_t, gid: gid_t, @@ -84,21 +112,28 @@ impl Drop for SwitchUserGuard { } } -/// Safely switch user and group for the current scope. -/// Requires root access. +/// Sets the **effective user** and the **effective group** for the current +/// scope. +/// +/// Typically, trying to switch to any user or group other than the ones already +/// running the process requires root privileges. +/// +/// **Use with care!** Possible security issues can happen, as Rust doesn't +/// guarantee running the destructor! If in doubt run `drop()` method on the +/// guard value manually! +/// +/// ### Examples +/// +/// ```no_run +/// use users::switch::switch_user_group; /// -/// ```ignore /// { /// let _guard = switch_user_group(1001, 1001); /// // current and effective user and group ids are 1001 /// } /// // back to the old values /// ``` -/// -/// Use with care! Possible security issues can happen, as Rust doesn't -/// guarantee running the destructor! If in doubt run `drop()` method -/// on the guard value manually! -pub fn switch_user_group(uid: uid_t, gid: gid_t) -> Result { +pub fn switch_user_group(uid: uid_t, gid: gid_t) -> IOResult { let current_state = SwitchUserGuard { uid: get_effective_uid(), gid: get_effective_gid(),