diff options
author | pwygab <88221256+merelymyself@users.noreply.github.com> | 2024-04-06 21:56:46 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-06 08:56:46 -0500 |
commit | 12b897b149a21d31eae133a148dacc3ea3d668e5 (patch) | |
tree | 93b7a946f7dd1fd9ec689d16d87c18d32c0fd50d /crates/nu-utils/src | |
parent | 16cbed7d6e93806d5b94bcbb90297b030b4796c9 (diff) |
Make auto-cd check for permissions (#12342)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx
you can also mention related issues, PRs or discussions!
-->
# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.
Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->
I was playing around with auto-cd and realised it didn't check for
permissions before cd'ing. This PR fixes that.
```
~/CodingProjects/nushell> /root
Error: nu::shell::io_error
× I/O error
help: Cannot change directory to /root: You are neither the owner, in the group, nor the super user and do not have permission
```
This PR also refactors some of the filesystem utilities to nu-utils,
specifically the permissions checking and users.
# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.
Make sure you've run and fixed any issues with these commands:
- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use std testing; testing run-tests --path
crates/nu-std"` to run the tests for the standard library
> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->
# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
Diffstat (limited to 'crates/nu-utils/src')
-rw-r--r-- | crates/nu-utils/src/filesystem.rs | 210 | ||||
-rw-r--r-- | crates/nu-utils/src/lib.rs | 4 |
2 files changed, 214 insertions, 0 deletions
diff --git a/crates/nu-utils/src/filesystem.rs b/crates/nu-utils/src/filesystem.rs new file mode 100644 index 000000000..588f0fccf --- /dev/null +++ b/crates/nu-utils/src/filesystem.rs @@ -0,0 +1,210 @@ +use std::path::Path; +#[cfg(unix)] +use { + nix::{ + sys::stat::{mode_t, Mode}, + unistd::{Gid, Uid}, + }, + std::os::unix::fs::MetadataExt, +}; + +// The result of checking whether we have permission to cd to a directory +#[derive(Debug)] +pub enum PermissionResult<'a> { + PermissionOk, + PermissionDenied(&'a str), +} + +// TODO: Maybe we should use file_attributes() from https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html +// More on that here: https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants +#[cfg(windows)] +pub fn have_permission(dir: impl AsRef<Path>) -> PermissionResult<'static> { + match dir.as_ref().read_dir() { + Err(e) => { + if matches!(e.kind(), std::io::ErrorKind::PermissionDenied) { + PermissionResult::PermissionDenied("Folder is unable to be read") + } else { + PermissionResult::PermissionOk + } + } + Ok(_) => PermissionResult::PermissionOk, + } +} + +#[cfg(unix)] +pub fn have_permission(dir: impl AsRef<Path>) -> PermissionResult<'static> { + match dir.as_ref().metadata() { + Ok(metadata) => { + let mode = Mode::from_bits_truncate(metadata.mode() as mode_t); + let current_user_uid = users::get_current_uid(); + if current_user_uid.is_root() { + return PermissionResult::PermissionOk; + } + let current_user_gid = users::get_current_gid(); + let owner_user = Uid::from_raw(metadata.uid()); + let owner_group = Gid::from_raw(metadata.gid()); + match ( + current_user_uid == owner_user, + current_user_gid == owner_group, + ) { + (true, _) => { + if mode.contains(Mode::S_IXUSR) { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are the owner but do not have execute permission", + ) + } + } + (false, true) => { + if mode.contains(Mode::S_IXGRP) { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are in the group but do not have execute permission", + ) + } + } + (false, false) => { + if mode.contains(Mode::S_IXOTH) + || (mode.contains(Mode::S_IXGRP) + && any_group(current_user_gid, owner_group)) + { + PermissionResult::PermissionOk + } else { + PermissionResult::PermissionDenied( + "You are neither the owner, in the group, nor the super user and do not have permission", + ) + } + } + } + } + Err(_) => PermissionResult::PermissionDenied("Could not retrieve file metadata"), + } +} + +#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] +fn any_group(_current_user_gid: Gid, owner_group: Gid) -> bool { + users::current_user_groups() + .unwrap_or_default() + .contains(&owner_group) +} + +#[cfg(all( + unix, + not(any(target_os = "linux", target_os = "freebsd", target_os = "android")) +))] +fn any_group(current_user_gid: Gid, owner_group: Gid) -> bool { + users::get_current_username() + .and_then(|name| users::get_user_groups(&name, current_user_gid)) + .unwrap_or_default() + .contains(&owner_group) +} + +#[cfg(unix)] +pub mod users { + use nix::unistd::{Gid, Group, Uid, User}; + + pub fn get_user_by_uid(uid: Uid) -> Option<User> { + User::from_uid(uid).ok().flatten() + } + + pub fn get_group_by_gid(gid: Gid) -> Option<Group> { + Group::from_gid(gid).ok().flatten() + } + + pub fn get_current_uid() -> Uid { + Uid::current() + } + + pub fn get_current_gid() -> Gid { + Gid::current() + } + + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] + pub fn get_current_username() -> Option<String> { + get_user_by_uid(get_current_uid()).map(|user| user.name) + } + + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "android"))] + pub fn current_user_groups() -> Option<Vec<Gid>> { + if let Ok(mut groups) = nix::unistd::getgroups() { + groups.sort_unstable_by_key(|id| id.as_raw()); + groups.dedup(); + Some(groups) + } else { + None + } + } + + /// Returns groups for a provided user name and primary group id. + /// + /// # libc functions used + /// + /// - [`getgrouplist`](https://docs.rs/libc/*/libc/fn.getgrouplist.html) + /// + /// # Examples + /// + /// ```ignore + /// use users::get_user_groups; + /// + /// for group in get_user_groups("stevedore", 1001).expect("Error looking up groups") { + /// println!("User is a member of group #{group}"); + /// } + /// ``` + #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "android")))] + pub fn get_user_groups(username: &str, gid: Gid) -> Option<Vec<Gid>> { + use nix::libc::{c_int, gid_t}; + use std::ffi::CString; + + // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons + #[cfg(target_os = "macos")] + let mut buff: Vec<i32> = vec![0; 1024]; + #[cfg(not(target_os = "macos"))] + let mut buff: Vec<gid_t> = vec![0; 1024]; + + let name = CString::new(username).ok()?; + + let mut count = buff.len() as c_int; + + // MacOS uses i32 instead of gid_t in getgrouplist for unknown reasons + // SAFETY: + // int getgrouplist(const char *user, gid_t group, gid_t *groups, int *ngroups); + // + // `name` is valid CStr to be `const char*` for `user` + // every valid value will be accepted for `group` + // The capacity for `*groups` is passed in as `*ngroups` which is the buffer max length/capacity (as we initialize with 0) + // Following reads from `*groups`/`buff` will only happen after `buff.truncate(*ngroups)` + #[cfg(target_os = "macos")] + let res = unsafe { + nix::libc::getgrouplist( + name.as_ptr(), + gid.as_raw() as i32, + buff.as_mut_ptr(), + &mut count, + ) + }; + + #[cfg(not(target_os = "macos"))] + let res = unsafe { + nix::libc::getgrouplist(name.as_ptr(), gid.as_raw(), buff.as_mut_ptr(), &mut count) + }; + + if res < 0 { + None + } else { + buff.truncate(count as usize); + buff.sort_unstable(); + buff.dedup(); + // allow trivial cast: on macos i is i32, on linux it's already gid_t + #[allow(trivial_numeric_casts)] + Some( + buff.into_iter() + .map(|id| Gid::from_raw(id as gid_t)) + .filter_map(get_group_by_gid) + .map(|group| group.gid) + .collect(), + ) + } + } +} diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index 575704dc8..00dcf3645 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -2,6 +2,7 @@ mod casing; pub mod ctrl_c; mod deansi; pub mod emoji; +pub mod filesystem; pub mod locale; pub mod utils; @@ -16,3 +17,6 @@ pub use deansi::{ strip_ansi_likely, strip_ansi_string_likely, strip_ansi_string_unlikely, strip_ansi_unlikely, }; pub use emoji::contains_emoji; + +#[cfg(unix)] +pub use filesystem::users; |