From 28ddc0c6673396f40241aa814f041ee71957a59a Mon Sep 17 00:00:00 2001 From: Ben S Date: Tue, 26 Jan 2016 20:23:16 +0000 Subject: [PATCH 01/21] Correct group ID type! --- src/base.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base.rs b/src/base.rs index dd643a4..8a038d2 100644 --- a/src/base.rs +++ b/src/base.rs @@ -86,7 +86,7 @@ pub struct User { pub struct Group { /// This group's ID - pub gid: uid_t, + pub gid: gid_t, /// This group's name pub name: Arc, From 476e31662bd81be57f679aa5e86e28543e4ba9aa Mon Sep 17 00:00:00 2001 From: Ben S Date: Tue, 26 Jan 2016 20:24:36 +0000 Subject: [PATCH 02/21] Begin to move OS routines to a new module This tries to mimic the Metadata struct in libc by providing a struct with hidden fields, then an OS-dependent trait that gives you accessors to those fields. Its goal is to remove any OS-specific things about users and groups -- which there will be a lot of -- from the default, publicly-accessible struct. This may not seem like a big problem, but you'll see it a lot in mocking code: "unimportant" fields, such as the user's home directory and shell, can now be mocked to include a "sensible" value even when they're not used. --- src/base.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 2 ++ src/mock.rs | 27 ++++++++++++++++----------- src/os.rs | 23 +++++++++++++++++++++++ 4 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 src/os.rs diff --git a/src/base.rs b/src/base.rs index 8a038d2..57f9546 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,4 +1,5 @@ use std::ffi::{CStr, CString}; +use std::path::Path; use std::ptr::read; use std::str::from_utf8_unchecked; use std::sync::Arc; @@ -11,6 +12,8 @@ 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)] @@ -75,10 +78,10 @@ pub struct User { pub primary_group: gid_t, /// This user's home directory - pub home_dir: String, + home_dir: String, /// This user's shell - pub shell: String, + shell: String, } /// Information about a particular group. @@ -95,6 +98,51 @@ pub struct Group { pub members: Vec, } +impl unix::UserExt for User { + fn home_dir(&self) -> &Path { + Path::new(&self.home_dir) + } + + fn with_home_dir(mut self, home_dir: &str) -> User { + self.home_dir = home_dir.to_owned(); + self + } + + fn shell(&self) -> &Path { + Path::new(&self.shell) + } + + fn with_shell(mut self, shell: &str) -> User { + self.shell = shell.to_owned(); + self + } + + fn new(uid: uid_t, name: &str, primary_group: gid_t) -> User { + User { + uid: uid, + name: Arc::new(name.to_owned()), + primary_group: primary_group, + home_dir: "/var/empty".to_owned(), + shell: "/bin/false".to_owned(), + } + } +} + +impl unix::GroupExt for Group { + fn members(&self) -> &[String] { + &*self.members + } + + fn new(gid: gid_t, name: &str) -> Group { + Group { + gid: gid, + name: Arc::new(name.to_owned()), + members: Vec::new(), + } + } +} + + unsafe fn from_raw_buf(p: *const i8) -> String { from_utf8_unchecked(CStr::from_ptr(p).to_bytes()).to_string() } diff --git a/src/lib.rs b/src/lib.rs index a3f185e..0a387dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,8 @@ pub use cache::UsersCache; pub mod mock; +pub mod os; + pub mod switch; mod traits; diff --git a/src/mock.rs b/src/mock.rs index 65c7c0d..9a42fa6 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(); @@ -144,15 +147,17 @@ impl Groups for MockUsers { #[cfg(test)] mod test { - use super::{MockUsers}; + use super::MockUsers; use base::{User, Group}; use traits::{Users, Groups}; use std::sync::Arc; + use os::unix::{UserExt, GroupExt}; + #[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,21 +170,21 @@ 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() }); + 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.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() }); + 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() }); + users.add_user(User::new(1337, "fred", 101)); assert_eq!(None, users.get_user_by_name("criminy").map(|u| u.uid)) } @@ -192,21 +197,21 @@ mod test { #[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![], }); + users.add_group(Group::new(1337, "fred")); assert_eq!(Some(Arc::new("fred".into())), users.get_group_by_gid(1337).map(|g| g.name.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![], }); + 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![], }); + users.add_group(Group::new(1337, "fred")); assert_eq!(None, users.get_group_by_name("santa").map(|g| g.gid)) } diff --git a/src/os.rs b/src/os.rs new file mode 100644 index 0000000..f50b035 --- /dev/null +++ b/src/os.rs @@ -0,0 +1,23 @@ + +pub mod unix { + use std::path::Path; + use libc::{uid_t, gid_t}; + + pub trait UserExt { + fn home_dir(&self) -> &Path; + fn with_home_dir(mut self, home_dir: &str) -> Self; + + fn shell(&self) -> &Path; + fn with_shell(mut self, shell: &str) -> Self; + + // TODO(ogham): Isn't it weird that the setters take a string slice, but + // the getters return a Path? + + fn new(uid: uid_t, name: &str, primary_group: gid_t) -> Self; + } + + pub trait GroupExt { + fn members(&self) -> &[String]; + fn new(gid: gid_t, name: &str) -> Self; + } +} \ No newline at end of file From 8e61c47f1436202eec335d01903b16518aaaa229 Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 16:32:31 +0000 Subject: [PATCH 03/21] Comment OS methods --- src/os.rs | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/src/os.rs b/src/os.rs index f50b035..aa368f0 100644 --- a/src/os.rs +++ b/src/os.rs @@ -1,23 +1,71 @@ +//! 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. + +/// 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. pub mod unix { use std::path::Path; use libc::{uid_t, gid_t}; + /// 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 a string slice, but - // the getters return a Path? + // TODO(ogham): Isn’t it weird that the setters take string slices, but + // the getters return paths? + /// 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. fn new(uid: uid_t, name: &str, primary_group: gid_t) -> Self; } + /// 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]; + + /// 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. fn new(gid: gid_t, name: &str) -> Self; } -} \ No newline at end of file +} From e32ec998e9d2646ed57edcba5e6ccc1f721f32dc Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 18:40:17 +0000 Subject: [PATCH 04/21] Switch to using from_utf8_lossy and document this - Replace the calls to from_utf8_unchecked to from_utf8_lossy. I don't think OSes will actually check the validity of these username strings, and I'm not sure what the best thing to do here is! Error out? - Remove the casts to *const i8. I think these stemmed from when I didn't know what I was doing. - Document what this module does --- src/base.rs | 51 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/base.rs b/src/base.rs index 57f9546..b8d327f 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,7 +1,37 @@ +//! 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::path::Path; use std::ptr::read; -use std::str::from_utf8_unchecked; use std::sync::Arc; use libc::{uid_t, gid_t}; @@ -142,9 +172,14 @@ impl unix::GroupExt for Group { } } - -unsafe fn from_raw_buf(p: *const i8) -> String { - from_utf8_unchecked(CStr::from_ptr(p).to_bytes()).to_string() +/// 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() } unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { @@ -152,10 +187,10 @@ unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { let pw = read(pointer); Some(User { uid: pw.pw_uid as uid_t, - name: Arc::new(from_raw_buf(pw.pw_name as *const i8)), + name: Arc::new(from_raw_buf(pw.pw_name)), 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) + home_dir: from_raw_buf(pw.pw_dir), + shell: from_raw_buf(pw.pw_shell) }) } else { @@ -166,7 +201,7 @@ 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 name = from_raw_buf(gr.gr_name); let members = members(gr.gr_mem); Some(Group { gid: gr.gr_gid, name: Arc::new(name), members: members }) } From 78c4b410dbe2b431f79b081471a149975e8d120f Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 19:09:49 +0000 Subject: [PATCH 05/21] Misc stylistic changes - Extract all the 'if ptr.is_null() .. else ..' logic into a function that's basically the ptr_as_ref feature. - Start using 'if let' syntax for some of these methods, which I'm not sure was around when I started writing them. - vec![] -> Vec::new() - loop -> for i in 0.. - Better comments, too --- src/base.rs | 122 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/src/base.rs b/src/base.rs index b8d327f..a5bb449 100644 --- a/src/base.rs +++ b/src/base.rs @@ -182,15 +182,33 @@ 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)); + let home_dir = from_raw_buf(passwd.pw_dir); + let shell = from_raw_buf(passwd.pw_shell); + Some(User { - uid: pw.pw_uid as uid_t, - name: Arc::new(from_raw_buf(pw.pw_name)), - primary_group: pw.pw_gid as gid_t, - home_dir: from_raw_buf(pw.pw_dir), - shell: from_raw_buf(pw.pw_shell) + uid: passwd.pw_uid, + name: name, + primary_group: passwd.pw_gid, + home_dir: home_dir, + shell: shell, }) } else { @@ -199,76 +217,96 @@ 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); - 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)); + let members = members(group.gr_mem); + + Some(Group { + gid: group.gr_gid, + name: name, + members: members, + }) } 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. From ce47457bba570908173b012d8af4293d29665f7c Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 19:27:50 +0000 Subject: [PATCH 06/21] Improve unreachable error messages --- src/switch.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/switch.rs b/src/switch.rs index 4cceac0..5414026 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -19,54 +19,54 @@ extern { /// Sets current user for the running process, requires root priviledges. 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. 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. 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. 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. 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. 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) } } From 8cfb4a0858969e7825b9365fdb6adb2be8d6a71b Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 19:41:17 +0000 Subject: [PATCH 07/21] At least parse the switch_user_group test It won't run, but it can at least be analysed to see if it works! --- src/switch.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/switch.rs b/src/switch.rs index 5414026..c99a2c1 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -87,7 +87,9 @@ impl Drop for SwitchUserGuard { /// Safely switch user and group for the current scope. /// Requires root access. /// -/// ```ignore +/// ```no_run +/// use users::switch::switch_user_group; +/// /// { /// let _guard = switch_user_group(1001, 1001); /// // current and effective user and group ids are 1001 From 5ac78b90f3f59fab2adfa88a4fe70e44792dbfe2 Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 19:44:12 +0000 Subject: [PATCH 08/21] Expand doc comments in switch module --- src/switch.rs | 54 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/switch.rs b/src/switch.rs index c99a2c1..caea9d1 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -16,7 +16,11 @@ 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(()), @@ -25,7 +29,11 @@ pub fn set_current_uid(uid: uid_t) -> IOResult<()> { } } -/// 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(()), @@ -34,7 +42,11 @@ pub fn set_current_gid(gid: gid_t) -> IOResult<()> { } } -/// 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(()), @@ -43,7 +55,11 @@ pub fn set_effective_uid(uid: uid_t) -> IOResult<()> { } } -/// 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(()), @@ -52,7 +68,11 @@ pub fn set_effective_gid(gid: gid_t) -> IOResult<()> { } } -/// 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(()), @@ -61,7 +81,12 @@ pub fn set_both_uid(ruid: uid_t, euid: uid_t) -> IOResult<()> { } } -/// 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(()), @@ -84,8 +109,17 @@ 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; @@ -96,10 +130,6 @@ impl Drop for SwitchUserGuard { /// } /// // 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 { let current_state = SwitchUserGuard { uid: get_effective_uid(), From 361bf319eb0e7b1df397d426cb8c89ae2f133c5d Mon Sep 17 00:00:00 2001 From: Ben S Date: Wed, 27 Jan 2016 19:44:41 +0000 Subject: [PATCH 09/21] Result -> IOResult --- src/switch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/switch.rs b/src/switch.rs index caea9d1..12d7a04 100644 --- a/src/switch.rs +++ b/src/switch.rs @@ -130,7 +130,7 @@ impl Drop for SwitchUserGuard { /// } /// // back to the old values /// ``` -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(), From d5a02504dbaa23fb39d05cd58ebb542cb5563bf6 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 08:52:13 +0000 Subject: [PATCH 10/21] Have Users work with its OS-dependent fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Everything relating to home directories and shell paths are now done in the UserExtras struct instead. I moved the OS module into the base module (and re-exported it in lib) because it's a "sub"-module of base—it can't work without the structs in base. --- src/base.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 11 +++-- src/os.rs | 71 --------------------------- 3 files changed, 128 insertions(+), 93 deletions(-) delete mode 100644 src/os.rs diff --git a/src/base.rs b/src/base.rs index a5bb449..682368d 100644 --- a/src/base.rs +++ b/src/base.rs @@ -47,7 +47,7 @@ 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 @@ -62,7 +62,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 @@ -73,7 +73,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 @@ -107,11 +107,7 @@ pub struct User { /// The ID of this user's primary group pub primary_group: gid_t, - /// This user's home directory - home_dir: String, - - /// This user's shell - shell: String, + extras: os::UserExtras, } /// Information about a particular group. @@ -130,20 +126,20 @@ pub struct Group { impl unix::UserExt for User { fn home_dir(&self) -> &Path { - Path::new(&self.home_dir) + Path::new(&self.extras.home_dir) } fn with_home_dir(mut self, home_dir: &str) -> User { - self.home_dir = home_dir.to_owned(); + self.extras.home_dir = home_dir.to_owned(); self } fn shell(&self) -> &Path { - Path::new(&self.shell) + Path::new(&self.extras.shell) } fn with_shell(mut self, shell: &str) -> User { - self.shell = shell.to_owned(); + self.extras.shell = shell.to_owned(); self } @@ -152,8 +148,7 @@ impl unix::UserExt for User { uid: uid, name: Arc::new(name.to_owned()), primary_group: primary_group, - home_dir: "/var/empty".to_owned(), - shell: "/bin/false".to_owned(), + extras: os::UserExtras::default(), } } } @@ -200,15 +195,12 @@ unsafe fn ptr_as_ref(pointer: *const T) -> Option { unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { if let Some(passwd) = ptr_as_ref(pointer) { let name = Arc::new(from_raw_buf(passwd.pw_name)); - let home_dir = from_raw_buf(passwd.pw_dir); - let shell = from_raw_buf(passwd.pw_shell); Some(User { uid: passwd.pw_uid, name: name, primary_group: passwd.pw_gid, - home_dir: home_dir, - shell: shell, + extras: os::UserExtras::from_passwd(passwd), }) } else { @@ -354,6 +346,113 @@ pub fn get_effective_groupname() -> Option { } + + +/// 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. + pub mod unix { + use std::path::Path; + use libc::{uid_t, gid_t}; + use super::super::{c_passwd, from_raw_buf}; + + /// 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? + + /// 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. + fn new(uid: uid_t, name: &str, primary_group: gid_t) -> Self; + } + + /// 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]; + + /// 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. + fn new(gid: gid_t, name: &str) -> Self; + } + + #[derive(Clone)] + pub struct UserExtras { + pub home_dir: String, + pub shell: String, + } + + impl Default for UserExtras { + fn default() -> UserExtras { + UserExtras { + home_dir: String::from("/var/empty"), + shell: String::from("/bin/false"), + } + } + } + + impl UserExtras { + 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 = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub type UserExtras = unix::UserExtras; +} + + #[cfg(test)] mod test { use super::*; @@ -386,11 +485,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] diff --git a/src/lib.rs b/src/lib.rs index 0a387dd..5a9939a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,15 +112,20 @@ 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; pub mod mock; -pub mod os; - pub mod switch; mod traits; diff --git a/src/os.rs b/src/os.rs deleted file mode 100644 index aa368f0..0000000 --- a/src/os.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! 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. - - -/// 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. -pub mod unix { - use std::path::Path; - use libc::{uid_t, gid_t}; - - /// 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? - - /// 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. - fn new(uid: uid_t, name: &str, primary_group: gid_t) -> Self; - } - - /// 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]; - - /// 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. - fn new(gid: gid_t, name: &str) -> Self; - } -} From 1c7d73c01c7218cfaab7e4529e34b92a6b2331d0 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 13:07:15 +0000 Subject: [PATCH 11/21] Move *all* the OS functionality inside modules --- src/base.rs | 140 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 52 deletions(-) diff --git a/src/base.rs b/src/base.rs index 682368d..01e56c3 100644 --- a/src/base.rs +++ b/src/base.rs @@ -30,7 +30,6 @@ //! functions. use std::ffi::{CStr, CString}; -use std::path::Path; use std::ptr::read; use std::sync::Arc; @@ -110,6 +109,23 @@ pub struct User { extras: os::UserExtras, } +impl User { + + /// 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::new(name.to_owned()), + primary_group: primary_group, + extras: os::UserExtras::default(), + } + } +} + /// Information about a particular group. #[derive(Clone)] pub struct Group { @@ -124,49 +140,6 @@ pub struct Group { pub members: Vec, } -impl unix::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 - } - - fn new(uid: uid_t, name: &str, primary_group: gid_t) -> User { - User { - uid: uid, - name: Arc::new(name.to_owned()), - primary_group: primary_group, - extras: os::UserExtras::default(), - } - } -} - -impl unix::GroupExt for Group { - fn members(&self) -> &[String] { - &*self.members - } - - fn new(gid: gid_t, name: &str) -> Group { - Group { - gid: gid, - name: Arc::new(name.to_owned()), - members: Vec::new(), - } - } -} - /// 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. /// @@ -370,10 +343,13 @@ pub mod os { /// 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 std::sync::Arc; + use libc::{uid_t, gid_t}; - use super::super::{c_passwd, from_raw_buf}; + use super::super::{c_passwd, from_raw_buf, User, Group}; /// Unix-specific extensions for `User`s. pub trait UserExt { @@ -396,13 +372,6 @@ pub mod os { // TODO(ogham): Isn’t it weird that the setters take string slices, but // the getters return paths? - - /// 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. - fn new(uid: uid_t, name: &str, primary_group: gid_t) -> Self; } /// Unix-specific extensions for `Group`s. @@ -446,10 +415,77 @@ pub mod os { } } } + + #[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 + } + } + + impl GroupExt for Group { + fn members(&self) -> &[String] { + &*self.members + } + + fn new(gid: gid_t, name: &str) -> Group { + Group { + gid: gid, + name: Arc::new(name.to_owned()), + members: Vec::new(), + } + } + } } #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub mod bsd { + use std::path::Path; + use libc::{uid_t, gid_t}; + use super::super::{c_passwd, from_raw_buf}; + + #[derive(Clone)] + pub struct UserExtras { + pub extras: super::unix::UserExtras, + } + + impl UserExtras { + pub unsafe fn from_passwd(passwd: c_passwd) -> UserExtras { + UserExtras { + extras: super::unix::UserExtras::from_passwd(passwd), + } + } + } + + impl Default for UserExtras { + fn default() -> UserExtras { + UserExtras { + extras: super::unix::UserExtras::default(), + } + } + } + } + + #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub type UserExtras = bsd::UserExtras; + + #[cfg(any(target_os = "linux"))] pub type UserExtras = unix::UserExtras; + } From 56e5e17bf43a643302a7c2c335f5bac54cae2dc7 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 14:07:18 +0000 Subject: [PATCH 12/21] Add missing impl for BSD extras --- src/base.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/base.rs b/src/base.rs index 01e56c3..44a533f 100644 --- a/src/base.rs +++ b/src/base.rs @@ -455,26 +455,52 @@ pub mod os { #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] pub mod bsd { use std::path::Path; - use libc::{uid_t, gid_t}; - use super::super::{c_passwd, from_raw_buf}; + use libc::{uid_t, gid_t, time_t}; + use super::super::{c_passwd, from_raw_buf, User}; #[derive(Clone)] pub struct UserExtras { pub extras: super::unix::UserExtras, + pub change: time_t, + pub expire: time_t, } impl UserExtras { 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 + } + } + impl Default for UserExtras { fn default() -> UserExtras { UserExtras { extras: super::unix::UserExtras::default(), + change: 0, + expire: 0, } } } From 107e0aee7b1d4d8773e5c433419780238feccd59 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 14:07:31 +0000 Subject: [PATCH 13/21] Add example that makes sure it works everywhere --- examples/os.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 examples/os.rs diff --git a/examples/os.rs b/examples/os.rs new file mode 100644 index 0000000..ef9951c --- /dev/null +++ b/examples/os.rs @@ -0,0 +1,27 @@ +extern crate users; +use users::{Users, Groups, UsersCache}; +use users::os::unix::UserExt; + +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()); + + 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); + } + } +} From 305b37e8c2deabc89a64550027f809d0c60090a3 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 14:13:25 +0000 Subject: [PATCH 14/21] Ignore breaking test on Linux --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5a9939a..dd1bac5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ //! //! 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'!"); From fefddc4a76884a67abc70db0867887d42b9fdca7 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:04:22 +0000 Subject: [PATCH 15/21] Replace User fields with getters The name() accessor is able to hide the fact that the user's actual name string is hidden in an Arc. (The Arc is still available, since it's needed for caching) --- examples/example.rs | 4 ++-- examples/os.rs | 4 ++-- examples/threading.rs | 2 +- src/base.rs | 37 +++++++++++++++++++++---------------- src/cache.rs | 8 ++++---- src/lib.rs | 4 ++-- src/mock.rs | 16 ++++++++-------- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index c2f34a5..f5c71bd 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -8,9 +8,9 @@ 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!"); + 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() { diff --git a/examples/os.rs b/examples/os.rs index ef9951c..d2634ba 100644 --- a/examples/os.rs +++ b/examples/os.rs @@ -9,11 +9,11 @@ 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()); println!("Your shell is {}", you.shell().display()); println!("Your home directory is {}", you.home_dir().display()); - let primary_group = cache.get_group_by_gid(you.primary_group).expect("No entry for your primary group!"); + 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() { 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 44a533f..f6b4b33 100644 --- a/src/base.rs +++ b/src/base.rs @@ -96,16 +96,9 @@ extern { /// Information about a particular user. #[derive(Clone)] pub struct User { - - /// This user's ID - pub uid: uid_t, - - /// This user's name - pub name: Arc, - - /// The ID of this user's primary group - pub primary_group: gid_t, - + uid: uid_t, + pub name_arc: Arc, + primary_group: gid_t, extras: os::UserExtras, } @@ -119,11 +112,23 @@ impl User { pub fn new(uid: uid_t, name: &str, primary_group: gid_t) -> User { User { uid: uid, - name: Arc::new(name.to_owned()), + name_arc: Arc::new(name.to_owned()), primary_group: primary_group, extras: os::UserExtras::default(), } } + + pub fn uid(&self) -> uid_t { + self.uid.clone() + } + + pub fn name(&self) -> &str { + &**self.name_arc + } + + pub fn primary_group_id(&self) -> gid_t { + self.primary_group.clone() + } } /// Information about a particular group. @@ -171,7 +176,7 @@ unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { Some(User { uid: passwd.pw_uid, - name: name, + name_arc: name, primary_group: passwd.pw_gid, extras: os::UserExtras::from_passwd(passwd), }) @@ -282,7 +287,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. @@ -293,7 +298,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. @@ -527,7 +532,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] @@ -563,7 +568,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"); diff --git a/src/cache.rs b/src/cache.rs index bd96834..c6027f5 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()) } } diff --git a/src/lib.rs b/src/lib.rs index dd1bac5..047d019 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 diff --git a/src/mock.rs b/src/mock.rs index 9a42fa6..6f4be3b 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -84,7 +84,7 @@ 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. @@ -99,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 { @@ -107,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 { @@ -115,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()) } } @@ -171,27 +171,27 @@ mod test { fn uid() { let mut users = MockUsers::with_current_uid(0); 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.clone())) + 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::new(1440, "fred", 101)); - assert_eq!(Some(1440), users.get_user_by_name("fred").map(|u| u.uid)) + 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::new(1337, "fred", 101)); - assert_eq!(None, users.get_user_by_name("criminy").map(|u| u.uid)) + 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] From b6ef295697db1f490d65e0b53f28e12de1d00d54 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:27:36 +0000 Subject: [PATCH 16/21] Make group members OS-dependent --- examples/example.rs | 9 ------- examples/os.rs | 6 ++--- src/base.rs | 64 +++++++++++++++++++++++++++++---------------- src/lib.rs | 8 ++---- 4 files changed, 46 insertions(+), 41 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index f5c71bd..d8cd69d 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -12,13 +12,4 @@ fn main() { 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.iter() { - println!("User {} is also a member of that group.", username); - } - } } diff --git a/examples/os.rs b/examples/os.rs index d2634ba..7a0a104 100644 --- a/examples/os.rs +++ b/examples/os.rs @@ -1,6 +1,6 @@ extern crate users; use users::{Users, Groups, UsersCache}; -use users::os::unix::UserExt; +use users::os::unix::{UserExt, GroupExt}; fn main() { let cache = UsersCache::new(); @@ -16,11 +16,11 @@ fn main() { 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() { + if primary_group.members().is_empty() { println!("There are no other members of that group."); } else { - for username in primary_group.members.iter() { + for username in primary_group.members() { println!("User {} is also a member of that group.", username); } } diff --git a/src/base.rs b/src/base.rs index f6b4b33..ae36f80 100644 --- a/src/base.rs +++ b/src/base.rs @@ -141,8 +141,25 @@ pub struct Group { /// 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, + extras: os::GroupExtras, + + // Vector of the names of the users who belong to this group as a non-primary member + //pub members: Vec, +} + +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::new(String::from(name)), + extras: os::GroupExtras::default(), + } + } } /// Reads data from a `*char` field in `c_passwd` or `g_group` into a UTF-8 @@ -172,7 +189,7 @@ unsafe fn ptr_as_ref(pointer: *const T) -> Option { unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { if let Some(passwd) = ptr_as_ref(pointer) { - let name = Arc::new(from_raw_buf(passwd.pw_name)); + let name = Arc::new(from_raw_buf(passwd.pw_name)); Some(User { uid: passwd.pw_uid, @@ -188,13 +205,12 @@ unsafe fn passwd_to_user(pointer: *const c_passwd) -> Option { unsafe fn struct_to_group(pointer: *const c_group) -> Option { if let Some(group) = ptr_as_ref(pointer) { - let name = Arc::new(from_raw_buf(group.gr_name)); - let members = members(group.gr_mem); + let name = Arc::new(from_raw_buf(group.gr_name)); Some(Group { gid: group.gr_gid, name: name, - members: members, + extras: os::GroupExtras::from_struct(group), }) } else { @@ -354,7 +370,7 @@ pub mod os { use std::sync::Arc; use libc::{uid_t, gid_t}; - use super::super::{c_passwd, from_raw_buf, User, Group}; + use super::super::{c_passwd, c_group, members, from_raw_buf, User, Group}; /// Unix-specific extensions for `User`s. pub trait UserExt { @@ -385,13 +401,6 @@ pub mod os { /// Returns a slice of the list of users that are in this group as /// their non-primary group. fn members(&self) -> &[String]; - - /// 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. - fn new(gid: gid_t, name: &str) -> Self; } #[derive(Clone)] @@ -442,17 +451,24 @@ pub mod os { } } + #[derive(Clone, Default)] + pub struct GroupExtras { + pub members: Vec, + } + + impl GroupExtras { + 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.members - } - - fn new(gid: gid_t, name: &str) -> Group { - Group { - gid: gid, - name: Arc::new(name.to_owned()), - members: Vec::new(), - } + &*self.extras.members } } } @@ -517,6 +533,8 @@ pub mod os { #[cfg(any(target_os = "linux"))] pub type UserExtras = unix::UserExtras; + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] + pub type GroupExtras = unix::GroupExtras; } diff --git a/src/lib.rs b/src/lib.rs index 047d019..5bdc415 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 @@ -80,11 +80,10 @@ //! ## 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: //! @@ -93,9 +92,6 @@ //! 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); -//! } //! ``` //! //! From b5c4d07f6b246baa009c872537de2069bccaecbe Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:39:04 +0000 Subject: [PATCH 17/21] Replace group fields with accessors, too This does the same thing: it hides the fact that a group's name is actually within an Arc. --- examples/example.rs | 2 +- examples/os.rs | 2 +- src/base.rs | 37 +++++++++++++++++++------------------ src/cache.rs | 8 ++++---- src/lib.rs | 2 +- src/mock.rs | 16 ++++++++-------- 6 files changed, 34 insertions(+), 33 deletions(-) diff --git a/examples/example.rs b/examples/example.rs index d8cd69d..2df0cf5 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -11,5 +11,5 @@ fn main() { println!("Your username is {}", you.name()); 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); + println!("Your primary group has ID {} and name {}", primary_group.gid(), primary_group.name()); } diff --git a/examples/os.rs b/examples/os.rs index 7a0a104..e538c45 100644 --- a/examples/os.rs +++ b/examples/os.rs @@ -14,7 +14,7 @@ fn main() { println!("Your home directory is {}", you.home_dir().display()); 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); + 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."); diff --git a/src/base.rs b/src/base.rs index ae36f80..90840d5 100644 --- a/src/base.rs +++ b/src/base.rs @@ -134,20 +134,13 @@ impl User { /// Information about a particular group. #[derive(Clone)] pub struct Group { - - /// This group's ID - pub gid: gid_t, - - /// This group's name - pub name: Arc, - + gid: gid_t, + pub name_arc: Arc, extras: os::GroupExtras, - - // Vector of the names of the users who belong to this group as a non-primary member - //pub members: Vec, } impl Group { + /// Create a new `Group` with the given group ID and name, with the /// rest of the fields filled in with dummy values. /// @@ -156,10 +149,18 @@ impl Group { pub fn new(gid: gid_t, name: &str) -> Self { Group { gid: gid, - name: Arc::new(String::from(name)), + name_arc: Arc::new(String::from(name)), extras: os::GroupExtras::default(), } } + + pub fn gid(&self) -> gid_t { + self.gid.clone() + } + + pub fn name(&self) -> &str { + &**self.name_arc + } } /// Reads data from a `*char` field in `c_passwd` or `g_group` into a UTF-8 @@ -208,9 +209,9 @@ unsafe fn struct_to_group(pointer: *const c_group) -> Option { let name = Arc::new(from_raw_buf(group.gr_name)); Some(Group { - gid: group.gr_gid, - name: name, - extras: os::GroupExtras::from_struct(group), + gid: group.gr_gid, + name_arc: name, + extras: os::GroupExtras::from_struct(group), }) } else { @@ -325,7 +326,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. @@ -336,7 +337,7 @@ 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()) } @@ -600,10 +601,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 c6027f5..54fbd34 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -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 5bdc415..63d9359 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ //! 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); +//! println!("The '{}' group has the ID {}", group.name(), group.gid()); //! ``` //! //! diff --git a/src/mock.rs b/src/mock.rs index 6f4be3b..682a83e 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -89,7 +89,7 @@ impl MockUsers { /// 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)) } } @@ -125,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 { @@ -133,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 { @@ -141,7 +141,7 @@ 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()) } } @@ -198,26 +198,26 @@ mod test { fn gid() { let mut users = MockUsers::with_current_uid(0); users.add_group(Group::new(1337, "fred")); - assert_eq!(Some(Arc::new("fred".into())), users.get_group_by_gid(1337).map(|g| g.name.clone())) + 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::new(1337, "fred")); - assert_eq!(Some(1337), users.get_group_by_name("fred").map(|g| g.gid)) + 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::new(1337, "fred")); - assert_eq!(None, users.get_group_by_name("santa").map(|g| g.gid)) + 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())) } } From cb2919660baf4ecb464d52d69147947923538e7b Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:40:53 +0000 Subject: [PATCH 18/21] Remove unused imports --- src/base.rs | 13 +++++++------ src/mock.rs | 2 -- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/base.rs b/src/base.rs index 90840d5..9ae556e 100644 --- a/src/base.rs +++ b/src/base.rs @@ -41,7 +41,7 @@ use libc::{c_char, time_t}; #[cfg(target_os = "linux")] use libc::c_char; -use os::*; +//use os::*; #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] @@ -368,10 +368,8 @@ pub mod os { #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] pub mod unix { use std::path::Path; - use std::sync::Arc; - use libc::{uid_t, gid_t}; - use super::super::{c_passwd, c_group, members, from_raw_buf, User, Group}; + use super::super::{c_passwd, c_group, members, from_raw_buf, Group}; /// Unix-specific extensions for `User`s. pub trait UserExt { @@ -431,6 +429,9 @@ pub mod os { } } + #[cfg(any(target_os = "linux"))] + use super::super::User; + #[cfg(any(target_os = "linux"))] impl UserExt for User { fn home_dir(&self) -> &Path { @@ -477,8 +478,8 @@ pub mod os { #[cfg(any(target_os = "macos", target_os = "freebsd", target_os = "dragonfly"))] pub mod bsd { use std::path::Path; - use libc::{uid_t, gid_t, time_t}; - use super::super::{c_passwd, from_raw_buf, User}; + use libc::time_t; + use super::super::{c_passwd, User}; #[derive(Clone)] pub struct UserExtras { diff --git a/src/mock.rs b/src/mock.rs index 682a83e..3712861 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -152,8 +152,6 @@ mod test { use traits::{Users, Groups}; use std::sync::Arc; - use os::unix::{UserExt, GroupExt}; - #[test] fn current_username() { let mut users = MockUsers::with_current_uid(1337); From e1f906d97c9b1b53bbf1d91938bad5e63fb44af3 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:52:53 +0000 Subject: [PATCH 19/21] Add missing lints and documentation --- src/base.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 5 +++++ src/switch.rs | 3 +++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/base.rs b/src/base.rs index 9ae556e..397af53 100644 --- a/src/base.rs +++ b/src/base.rs @@ -97,9 +97,12 @@ extern { #[derive(Clone)] pub struct User { uid: uid_t, - pub name_arc: Arc, primary_group: gid_t, extras: os::UserExtras, + + /// 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, } impl User { @@ -118,14 +121,17 @@ impl User { } } + /// Returns this user’s ID. pub fn uid(&self) -> uid_t { self.uid.clone() } + /// 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() } @@ -135,8 +141,11 @@ impl User { #[derive(Clone)] pub struct Group { gid: gid_t, - pub name_arc: Arc, extras: os::GroupExtras, + + /// 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, } impl Group { @@ -154,10 +163,12 @@ impl Group { } } + /// 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 } @@ -402,9 +413,14 @@ pub mod os { 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, } @@ -418,6 +434,8 @@ pub mod os { } 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); @@ -453,12 +471,17 @@ pub mod os { } } + /// 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); @@ -475,20 +498,34 @@ pub mod os { } } + /// 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, @@ -529,12 +566,15 @@ pub mod os { } } + /// 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; } diff --git a/src/lib.rs b/src/lib.rs index 63d9359..8df22d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -104,6 +104,11 @@ //! 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}; diff --git a/src/switch.rs b/src/switch.rs index 12d7a04..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}; @@ -95,6 +97,7 @@ pub fn set_both_gid(rgid: gid_t, egid: gid_t) -> IOResult<()> { } } +/// Guard returned from a `switch_user_group` call. pub struct SwitchUserGuard { uid: uid_t, gid: gid_t, From d602c1a6d59db694df5e8dc07dbf296866901585 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 15:56:55 +0000 Subject: [PATCH 20/21] Update the readme's examples to match --- README.md | 55 ++++++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) 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); ``` From d65d2180d9a66800a604fba185f881ea504b0375 Mon Sep 17 00:00:00 2001 From: Ben S Date: Thu, 28 Jan 2016 16:06:54 +0000 Subject: [PATCH 21/21] Add change and expire accessors --- examples/os.rs | 6 ++++++ src/base.rs | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/examples/os.rs b/examples/os.rs index e538c45..5829625 100644 --- a/examples/os.rs +++ b/examples/os.rs @@ -1,6 +1,7 @@ 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(); @@ -13,6 +14,11 @@ fn main() { 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()); diff --git a/src/base.rs b/src/base.rs index 397af53..11650fd 100644 --- a/src/base.rs +++ b/src/base.rs @@ -555,6 +555,26 @@ pub mod os { } } + /// 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 {