diff --git a/Cargo.lock b/Cargo.lock index e4e89939a2..043382112f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3615,6 +3615,7 @@ dependencies = [ name = "nu-plugin-protocol" version = "0.100.1" dependencies = [ + "nu-engine", "nu-protocol", "nu-utils", "rmp-serde", @@ -3871,6 +3872,17 @@ dependencies = [ "webpage", ] +[[package]] +name = "nu_plugin_selinux" +version = "0.100.1" +dependencies = [ + "nu-command", + "nu-plugin", + "nu-plugin-test-support", + "nu-protocol", + "selinux", +] + [[package]] name = "nu_plugin_stress_internals" version = "0.100.1" diff --git a/Cargo.toml b/Cargo.toml index e3ecadc4eb..44c751847f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ members = [ "crates/nu_plugin_custom_values", "crates/nu_plugin_formats", "crates/nu_plugin_polars", + "crates/nu_plugin_selinux", "crates/nu_plugin_stress_internals", "crates/nu-std", "crates/nu-table", diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 335269c4bb..e32ac8c4f2 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -1073,11 +1073,10 @@ fn flag_completions() { // Test completions for the 'ls' flags let suggestions = completer.complete("ls -", 4); - assert_eq!(20, suggestions.len()); + assert_eq!(18, suggestions.len()); let expected: Vec = vec![ "--all".into(), - "--context".into(), "--directory".into(), "--du".into(), "--full-paths".into(), @@ -1095,7 +1094,6 @@ fn flag_completions() { "-m".into(), "-s".into(), "-t".into(), - "-Z".into(), ]; // Match results diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 6abb2210a5..dedb57a211 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -21,6 +21,8 @@ use std::{ #[derive(Clone)] pub struct Ls; +pub type EntryMapper = (dyn Fn(&std::path::Path, Value) -> Value + Send + Sync); + #[derive(Clone, Copy)] struct Args { all: bool, @@ -31,12 +33,66 @@ struct Args { directory: bool, use_mime_type: bool, use_threads: bool, - security_context: bool, call_span: Span, } const GLOB_CHARS: &[char] = &['*', '?', '[']; +impl Ls { + pub fn run_ls( + call_span: Span, + get_signals: &(dyn Fn() -> Signals), + has_flag: &(dyn Fn(&str) -> Result), + has_pattern_arg: bool, + pattern_arg: Vec>>, + cwd: PathBuf, + map_entry: &EntryMapper, + ) -> Result { + let args = Args { + all: has_flag("all")?, + long: has_flag("long")?, + short_names: has_flag("short-names")?, + full_paths: has_flag("full-paths")?, + du: has_flag("du")?, + directory: has_flag("directory")?, + use_mime_type: has_flag("mime-type")?, + use_threads: has_flag("threads")?, + call_span, + }; + + let input_pattern_arg = match has_pattern_arg { + true if pattern_arg.is_empty() => vec![], + true => pattern_arg.clone(), + false => vec![None], + }; + + let mut result_iters = vec![]; + for pat in input_pattern_arg { + result_iters.push(ls_for_one_pattern( + pat, + args, + get_signals(), + cwd.clone(), + map_entry, + )?) + } + + // Here nushell needs to use + // use `flatten` to chain all iterators into one. + Ok(result_iters + .into_iter() + .flatten() + .into_pipeline_data_with_metadata( + call_span, + get_signals(), + PipelineMetadata { + data_source: DataSource::Ls, + content_type: None, + }, + )) + } +} + impl Command for Ls { fn name(&self) -> &str { "ls" @@ -50,6 +106,34 @@ impl Command for Ls { vec!["dir"] } + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let get_signals = &|| engine_state.signals().clone(); + let has_flag = &|flag: &str| call.has_flag(engine_state, &mut (stack.clone()), flag); + let pattern_arg = call + .rest::>(engine_state, &mut stack.clone(), 0)? + .into_iter() + .map(Some) + .collect(); + let has_pattern_arg = call.has_positional_args(stack, 0); + #[allow(deprecated)] + let cwd = current_dir(engine_state, stack)?; + Ls::run_ls( + call.span(), + get_signals, + has_flag, + has_pattern_arg, + pattern_arg, + cwd, + &|_path, entry| entry, + ) + } + fn signature(&self) -> nu_protocol::Signature { Signature::build("ls") .input_output_types(vec![(Type::Nothing, Type::table())]) @@ -59,7 +143,7 @@ impl Command for Ls { .switch("all", "Show hidden files", Some('a')) .switch( "long", - "Get almost all available columns for each entry (slower; columns are platform-dependent)", + "Get all available columns for each entry (slower; columns are platform-dependent)", Some('l'), ) .switch( @@ -80,96 +164,9 @@ impl Command for Ls { ) .switch("mime-type", "Show mime-type in type column instead of 'file' (based on filenames only; files' contents are not examined)", Some('m')) .switch("threads", "Use multiple threads to list contents. Output will be non-deterministic.", Some('t')) - .switch( - "context", - match cfg!(feature = "selinux") { - true => "Get the SELinux security context for each entry, if available", - false => "Get the SELinux security context for each entry (disabled)" - }, - Some('Z'), - ) .category(Category::FileSystem) } - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - _input: PipelineData, - ) -> Result { - let all = call.has_flag(engine_state, stack, "all")?; - let long = call.has_flag(engine_state, stack, "long")?; - let short_names = call.has_flag(engine_state, stack, "short-names")?; - let full_paths = call.has_flag(engine_state, stack, "full-paths")?; - let du = call.has_flag(engine_state, stack, "du")?; - let directory = call.has_flag(engine_state, stack, "directory")?; - let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?; - let use_threads = call.has_flag(engine_state, stack, "threads")?; - let security_context = call.has_flag(engine_state, stack, "context")?; - let call_span = call.head; - #[allow(deprecated)] - let cwd = current_dir(engine_state, stack)?; - - let args = Args { - all, - long, - short_names, - full_paths, - du, - directory, - use_mime_type, - use_threads, - security_context, - call_span, - }; - - let pattern_arg = call.rest::>(engine_state, stack, 0)?; - let input_pattern_arg = if !call.has_positional_args(stack, 0) { - None - } else { - Some(pattern_arg) - }; - match input_pattern_arg { - None => Ok( - ls_for_one_pattern(None, args, engine_state.signals().clone(), cwd)? - .into_pipeline_data_with_metadata( - call_span, - engine_state.signals().clone(), - PipelineMetadata { - data_source: DataSource::Ls, - content_type: None, - }, - ), - ), - Some(pattern) => { - let mut result_iters = vec![]; - for pat in pattern { - result_iters.push(ls_for_one_pattern( - Some(pat), - args, - engine_state.signals().clone(), - cwd.clone(), - )?) - } - - // Here nushell needs to use - // use `flatten` to chain all iterators into one. - Ok(result_iters - .into_iter() - .flatten() - .into_pipeline_data_with_metadata( - call_span, - engine_state.signals().clone(), - PipelineMetadata { - data_source: DataSource::Ls, - content_type: None, - }, - )) - } - } - } - fn examples(&self) -> Vec { vec![ Example { @@ -234,6 +231,7 @@ fn ls_for_one_pattern( args: Args, signals: Signals, cwd: PathBuf, + map_entry: &EntryMapper, ) -> Result { fn create_pool(num_threads: usize) -> Result { match rayon::ThreadPoolBuilder::new() @@ -263,7 +261,6 @@ fn ls_for_one_pattern( use_mime_type, use_threads, call_span, - security_context, } = args; let pattern_arg = { if let Some(path) = pattern_arg { @@ -450,8 +447,8 @@ fn ls_for_one_pattern( du, &signals_clone, use_mime_type, - args.full_paths, - security_context, + full_paths, + map_entry, ); match entry { Ok(value) => Some(value), @@ -570,7 +567,7 @@ pub(crate) fn dir_entry_dict( signals: &Signals, use_mime_type: bool, full_symlink_target: bool, - security_context: bool, + map_entry: &EntryMapper, ) -> Result { #[cfg(windows)] if metadata.is_none() { @@ -627,13 +624,6 @@ pub(crate) fn dir_entry_dict( } } - if security_context { - record.push( - "security_context", - security_context_value(filename, span).unwrap_or(Value::nothing(span)), // TODO: consider report_shell_warning - ) - } - if long { if let Some(md) = metadata { record.push("readonly", Value::bool(md.permissions().readonly(), span)); @@ -754,7 +744,7 @@ pub(crate) fn dir_entry_dict( record.push("modified", Value::nothing(span)); } - Ok(Value::record(record, span)) + Ok(map_entry(filename, Value::record(record, span))) } // TODO: can we get away from local times in `ls`? internals might be cleaner if we worked in UTC @@ -786,28 +776,6 @@ fn try_convert_to_local_date_time(t: SystemTime) -> Option> { } } -fn security_context_value(_path: &Path, span: Span) -> Result { - #[cfg(not(all(feature = "selinux", target_os = "linux")))] - return Ok(Value::nothing(span)); - - #[cfg(all(feature = "selinux", target_os = "linux"))] - { - use selinux; - match selinux::SecurityContext::of_path(_path, false, false) - .map_err(|e| ShellError::IOError { msg: e.to_string() })? - { - Some(con) => { - let bytes = con.as_bytes(); - Ok(Value::string( - String::from_utf8_lossy(&bytes[0..bytes.len().saturating_sub(1)]), - span, - )) - } - None => Ok(Value::nothing(span)), - } - } -} - // #[cfg(windows)] is just to make Clippy happy, remove if you ever want to use this on other platforms #[cfg(windows)] fn unix_time_to_local_date_time(secs: i64) -> Option> { diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 089e899dda..aa0125f63e 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -19,6 +19,7 @@ pub use self::open::Open; pub use cd::Cd; pub use du::Du; pub use glob::Glob; +pub use ls::EntryMapper as LsEntryMapper; pub use ls::Ls; pub use mktemp::Mktemp; pub use rm::Rm; diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index 85af92e452..416a6aaf7f 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -862,17 +862,3 @@ fn consistent_list_order() { assert_eq!(no_arg.out, with_arg.out); }) } - -#[cfg(all(feature = "selinux", target_os = "linux"))] -#[test] -fn returns_correct_security_context() { - use nu_test_support::nu_with_std; - - let input = " - use std assert - ^ls -Z / | lines | each { |e| $e | str trim | split column ' ' 'coreutils_scontext' 'name' | first } \ - | join (ls -Z / | each { default '?' security_context }) name \ - | each { |e| assert equal $e.security_context $e.coreutils_scontext $'For entry ($e.name) expected ($e.coreutils_scontext), got ($e.security_context)' } - "; - assert_eq!(nu_with_std!(input).err, ""); -} diff --git a/crates/nu-plugin-core/src/serializers/tests.rs b/crates/nu-plugin-core/src/serializers/tests.rs index b27f852ee4..948258119f 100644 --- a/crates/nu-plugin-core/src/serializers/tests.rs +++ b/crates/nu-plugin-core/src/serializers/tests.rs @@ -115,6 +115,7 @@ macro_rules! generate_tests { Value::float(1.0, Span::new(0, 10)), Value::string("something", Span::new(0, 10)), ], + has_positional_args: true, named: vec![( Spanned { item: "name".to_string(), diff --git a/crates/nu-plugin-engine/src/interface/tests.rs b/crates/nu-plugin-engine/src/interface/tests.rs index 4761da6824..d987d061bd 100644 --- a/crates/nu-plugin-engine/src/interface/tests.rs +++ b/crates/nu-plugin-engine/src/interface/tests.rs @@ -809,6 +809,7 @@ fn interface_write_plugin_call_writes_run_with_value_input() -> Result<(), Shell call: EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }, input: PipelineData::Value(Value::test_int(-1), Some(metadata0.clone())), @@ -850,6 +851,7 @@ fn interface_write_plugin_call_writes_run_with_stream_input() -> Result<(), Shel call: EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }, input: values @@ -1078,6 +1080,7 @@ fn interface_run() -> Result<(), ShellError> { call: EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }, input: PipelineData::Empty, @@ -1351,6 +1354,7 @@ fn prepare_plugin_call_run() { call: EvaluatedCall { head: span, positional: vec![Value::test_int(4)], + has_positional_args: true, named: vec![("x".to_owned().into_spanned(span), Some(Value::test_int(6)))], }, input: PipelineData::Empty, @@ -1363,6 +1367,7 @@ fn prepare_plugin_call_run() { call: EvaluatedCall { head: span, positional: vec![cv_ok.clone()], + has_positional_args: true, named: vec![("ok".to_owned().into_spanned(span), Some(cv_ok.clone()))], }, input: PipelineData::Empty, @@ -1375,6 +1380,7 @@ fn prepare_plugin_call_run() { call: EvaluatedCall { head: span, positional: vec![cv_bad.clone()], + has_positional_args: true, named: vec![], }, input: PipelineData::Empty, @@ -1387,6 +1393,7 @@ fn prepare_plugin_call_run() { call: EvaluatedCall { head: span, positional: vec![], + has_positional_args: false, named: vec![("bad".to_owned().into_spanned(span), Some(cv_bad.clone()))], }, input: PipelineData::Empty, @@ -1399,6 +1406,7 @@ fn prepare_plugin_call_run() { call: EvaluatedCall { head: span, positional: vec![], + has_positional_args: false, named: vec![], }, // Shouldn't check input - that happens somewhere else diff --git a/crates/nu-plugin-protocol/Cargo.toml b/crates/nu-plugin-protocol/Cargo.toml index 7659fec8d1..9afbd2f1a5 100644 --- a/crates/nu-plugin-protocol/Cargo.toml +++ b/crates/nu-plugin-protocol/Cargo.toml @@ -14,6 +14,7 @@ bench = false workspace = true [dependencies] +nu-engine = { path = "../nu-engine", version = "0.100.1" } nu-protocol = { path = "../nu-protocol", version = "0.100.1", features = ["plugin"] } nu-utils = { path = "../nu-utils", version = "0.100.1" } diff --git a/crates/nu-plugin-protocol/src/evaluated_call.rs b/crates/nu-plugin-protocol/src/evaluated_call.rs index c5cbcc5ba0..423a1a145d 100644 --- a/crates/nu-plugin-protocol/src/evaluated_call.rs +++ b/crates/nu-plugin-protocol/src/evaluated_call.rs @@ -1,3 +1,4 @@ +use nu_engine::CallExt; use nu_protocol::{ ast::{self, Expression}, engine::{Call, CallImpl, EngineState, Stack}, @@ -22,6 +23,8 @@ pub struct EvaluatedCall { pub head: Span, /// Values of positional arguments pub positional: Vec, + /// Whether positional arguments were used, including spreads + pub has_positional_args: bool, /// Names and values of named arguments pub named: Vec<(Spanned, Option)>, } @@ -32,6 +35,7 @@ impl EvaluatedCall { EvaluatedCall { head, positional: vec![], + has_positional_args: false, named: vec![], } } @@ -49,6 +53,7 @@ impl EvaluatedCall { /// ``` pub fn add_positional(&mut self, value: Value) -> &mut Self { self.positional.push(value); + self.has_positional_args = true; self } @@ -88,6 +93,7 @@ impl EvaluatedCall { /// Builder variant of [`.add_positional()`](Self::add_positional). pub fn with_positional(mut self, value: Value) -> Self { self.add_positional(value); + self.has_positional_args = true; self } @@ -144,6 +150,7 @@ impl EvaluatedCall { Ok(Self { head: call.head, positional, + has_positional_args: call.has_positional_args(stack, 0), named, }) } @@ -160,6 +167,7 @@ impl EvaluatedCall { Ok(Self { head: call.head, positional, + has_positional_args: call.has_positional_args(stack, 0), named, }) } @@ -178,6 +186,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # None @@ -194,6 +203,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "bar".to_owned(), span: null_span}, /// # None @@ -210,6 +220,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::bool(true, Span::unknown())) @@ -226,6 +237,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::bool(false, Span::unknown())) @@ -242,6 +254,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: true, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::int(1, Span::unknown())) @@ -289,6 +302,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: true, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::int(123, null_span)) @@ -310,6 +324,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![], /// # }; /// let opt_foo = match call.get_flag_value("foo") { @@ -344,6 +359,7 @@ impl EvaluatedCall { /// # Value::string("b".to_owned(), null_span), /// # Value::string("c".to_owned(), null_span), /// # ], + /// # has_positional_args: true, /// # named: vec![], /// # }; /// let arg = match call.nth(1) { @@ -370,6 +386,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::int(123, null_span)) @@ -387,6 +404,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "bar".to_owned(), span: null_span}, /// # Some(Value::int(123, null_span)) @@ -404,6 +422,7 @@ impl EvaluatedCall { /// # let call = EvaluatedCall { /// # head: null_span, /// # positional: Vec::new(), + /// # has_positional_args: false, /// # named: vec![( /// # Spanned { item: "foo".to_owned(), span: null_span}, /// # Some(Value::string("abc".to_owned(), null_span)) @@ -436,6 +455,7 @@ impl EvaluatedCall { /// # Value::string("two".to_owned(), null_span), /// # Value::string("three".to_owned(), null_span), /// # ], + /// # has_positional_args: true, /// # named: Vec::new(), /// # }; /// let args = call.rest::(0); @@ -497,6 +517,7 @@ mod test { Value::float(1.0, Span::new(0, 10)), Value::string("something", Span::new(0, 10)), ], + has_positional_args: true, named: vec![ ( Spanned { diff --git a/crates/nu-plugin/src/plugin/interface/tests.rs b/crates/nu-plugin/src/plugin/interface/tests.rs index 5ff9f6f6cd..78fdfb3133 100644 --- a/crates/nu-plugin/src/plugin/interface/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/tests.rs @@ -372,6 +372,7 @@ fn manager_consume_call_run_forwards_to_receiver_with_context() -> Result<(), Sh call: EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }, input: PipelineDataHeader::Empty, @@ -406,6 +407,7 @@ fn manager_consume_call_run_forwards_to_receiver_with_pipeline_data() -> Result< call: EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }, input: PipelineDataHeader::list_stream(ListStreamInfo::new(6, Span::test_data())), @@ -450,6 +452,7 @@ fn manager_consume_call_run_deserializes_custom_values_in_args() -> Result<(), S call: EvaluatedCall { head: Span::test_data(), positional: vec![value.clone()], + has_positional_args: true, named: vec![( Spanned { item: "flag".into(), diff --git a/crates/nu_plugin_query/src/query_xml.rs b/crates/nu_plugin_query/src/query_xml.rs index 5f46ce4355..0328cf7842 100644 --- a/crates/nu_plugin_query/src/query_xml.rs +++ b/crates/nu_plugin_query/src/query_xml.rs @@ -139,6 +139,7 @@ mod tests { let call = EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }; @@ -168,6 +169,7 @@ mod tests { let call = EvaluatedCall { head: Span::test_data(), positional: vec![], + has_positional_args: false, named: vec![], }; diff --git a/crates/nu_plugin_selinux/Cargo.toml b/crates/nu_plugin_selinux/Cargo.toml new file mode 100644 index 0000000000..78e3ecbc9a --- /dev/null +++ b/crates/nu_plugin_selinux/Cargo.toml @@ -0,0 +1,27 @@ +[package] +authors = ["The Nushell Project Developers"] +description = "Nushell commands with SELinux functionality." +edition = "2021" +license = "MIT" +name = "nu_plugin_selinux" +repository = "https://github.com/nushell/nushell/tree/main/crates/nu_plugin_selinux" +version = "0.100.1" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[bin]] +name = "nu_plugin_selinux" +bench = false + +[lib] +bench = false + +[dependencies] +nu-protocol = { path = "../nu-protocol", version = "0.100.1" } +nu-plugin = { path = "../nu-plugin", version = "0.100.1" } +nu-command = { path = "../nu-command", version = "0.100.1" } + +selinux = "0.4.6" + +[dev-dependencies] +nu-plugin-test-support = { path = "../nu-plugin-test-support", version = "0.100.1" } diff --git a/crates/nu_plugin_selinux/LICENSE b/crates/nu_plugin_selinux/LICENSE new file mode 100644 index 0000000000..57c5640054 --- /dev/null +++ b/crates/nu_plugin_selinux/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 The Nushell Project Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/nu_plugin_selinux/src/commands/ls.rs b/crates/nu_plugin_selinux/src/commands/ls.rs new file mode 100644 index 0000000000..d7e7d3f1cc --- /dev/null +++ b/crates/nu_plugin_selinux/src/commands/ls.rs @@ -0,0 +1,152 @@ +use std::path::Path; + +use nu_command::{Ls, LsEntryMapper}; +use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; +use nu_protocol::{engine::Command, Example, LabeledError, PipelineData, ShellError, Span, Value}; + +use crate::SELinuxPlugin; + +#[derive(Clone)] +pub struct SELinuxLs { + pub ls: Ls, +} + +impl PluginCommand for SELinuxLs { + type Plugin = SELinuxPlugin; + + fn name(&self) -> &str { + "selinux ls" + } + + fn description(&self) -> &str { + self.ls.description() + } + + fn search_terms(&self) -> Vec<&str> { + self.ls.search_terms() + } + + fn signature(&self) -> nu_protocol::Signature { + let mut signature = self.ls.signature().switch( + "context", + "Get the SELinux security context for each entry, if available", + Some('Z'), + ); + signature.name = "selinux ls".into(); + signature + } + + fn run( + &self, + _plugin: &SELinuxPlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: PipelineData, + ) -> Result { + let security_context = call.has_flag("context")?; + + let get_signals = &|| engine.signals().clone(); + let has_flag = &|flag: &str| call.has_flag(flag); + let has_pattern_arg = call.has_positional_args; + let pattern_arg = call.rest(0)?; + let cwd = engine.get_current_dir()?.into(); + let call_head = call.head; + let map_entry: &LsEntryMapper = &move |path, record| { + match record { + Value::Record { val, internal_span } if security_context => { + let mut val = val.into_owned(); + val.push( + "security_context", + security_context_value(path, call_head) + .unwrap_or(Value::nothing(call_head)), // TODO: consider report_shell_warning + ); + + Value::record(val, internal_span) + } + _ => record, + } + }; + let data = Ls::run_ls( + call_head, + get_signals, + has_flag, + has_pattern_arg, + pattern_arg, + cwd, + map_entry, + )?; + Ok(data) + } + + fn examples(&self) -> Vec { + self.ls.examples() + } +} + +fn security_context_value(_path: &Path, span: Span) -> Result { + #[cfg(not(target_os = "linux"))] + return Ok(Value::nothing(span)); + + #[cfg(target_os = "linux")] + { + use selinux; + match selinux::SecurityContext::of_path(_path, false, false) + .map_err(|e| ShellError::IOError { msg: e.to_string() })? + { + Some(con) => { + let bytes = con.as_bytes(); + Ok(Value::string( + String::from_utf8_lossy(&bytes[0..bytes.len().saturating_sub(1)]), + span, + )) + } + None => Ok(Value::nothing(span)), + } + } +} + +#[cfg(test)] +#[cfg(target_os = "linux")] +mod test { + use crate::SELinuxPlugin; + + use nu_command::{All, Each, External, First, Join, Lines, SplitColumn, StrTrim}; + use nu_plugin_test_support::PluginTest; + use nu_protocol::{engine::Command, ShellError, Value}; + use std::{env, sync::Arc}; + + #[test] + fn returns_correct_security_context() -> Result<(), ShellError> { + let plugin: Arc = Arc::new(SELinuxPlugin {}); + let mut plugin_test = PluginTest::new("selinux", plugin)?; + + let engine_state = plugin_test.engine_state_mut(); + engine_state.add_env_var("PWD".to_owned(), Value::test_string("/")); + engine_state.add_env_var("PATH".into(), Value::test_string(env::var("PATH").unwrap())); + + let deps: Vec> = vec![ + Box::new(External), + Box::new(Lines), + Box::new(Each), + Box::new(StrTrim), + Box::new(SplitColumn), + Box::new(First), + Box::new(Join), + Box::new(All), + ]; + for decl in deps { + plugin_test.add_decl(decl)?; + } + let input = " + ^ls -Z / | lines | each { |e| $e | str trim | split column ' ' 'coreutils_scontext' 'name' | first } \ + | join (selinux ls -sZ /) name \ + | all { |e| + let valid = $e.coreutils_scontext == '?' or $e.security_context == $e.coreutils_scontext + if not $valid { + error make { msg: $'For entry ($e.name) expected ($e.coreutils_scontext), got ($e.security_context)' } + } + }"; + plugin_test.eval(input)?; + Ok(()) + } +} diff --git a/crates/nu_plugin_selinux/src/commands/mod.rs b/crates/nu_plugin_selinux/src/commands/mod.rs new file mode 100644 index 0000000000..fe3230b70f --- /dev/null +++ b/crates/nu_plugin_selinux/src/commands/mod.rs @@ -0,0 +1,3 @@ +mod ls; + +pub use ls::SELinuxLs; diff --git a/crates/nu_plugin_selinux/src/lib.rs b/crates/nu_plugin_selinux/src/lib.rs new file mode 100644 index 0000000000..707c708130 --- /dev/null +++ b/crates/nu_plugin_selinux/src/lib.rs @@ -0,0 +1,15 @@ +mod commands; +pub use commands::*; +use nu_command::Ls; +use nu_plugin::{Plugin, PluginCommand}; +pub struct SELinuxPlugin; + +impl Plugin for SELinuxPlugin { + fn version(&self) -> String { + env!("CARGO_PKG_VERSION").into() + } + + fn commands(&self) -> Vec>> { + vec![Box::new(SELinuxLs { ls: Ls })] + } +} diff --git a/crates/nu_plugin_selinux/src/main.rs b/crates/nu_plugin_selinux/src/main.rs new file mode 100644 index 0000000000..515a779011 --- /dev/null +++ b/crates/nu_plugin_selinux/src/main.rs @@ -0,0 +1,6 @@ +use nu_plugin::{serve_plugin, MsgPackSerializer}; +use nu_plugin_selinux::SELinuxPlugin; + +fn main() { + serve_plugin(&SELinuxPlugin {}, MsgPackSerializer {}) +} diff --git a/scripts/build-all.nu b/scripts/build-all.nu index a340dbae70..acf63f609b 100644 --- a/scripts/build-all.nu +++ b/scripts/build-all.nu @@ -33,7 +33,8 @@ let plugins = [ nu_plugin_example, nu_plugin_custom_values, nu_plugin_formats, - nu_plugin_polars + nu_plugin_polars, + nu_plugin_selinux ] for plugin in $plugins { diff --git a/toolkit.nu b/toolkit.nu index d848dce18e..338ca74435 100644 --- a/toolkit.nu +++ b/toolkit.nu @@ -356,6 +356,7 @@ export def build [ nu_plugin_example, nu_plugin_custom_values, nu_plugin_formats, + nu_plugin_selinux ] for plugin in $plugins { @@ -428,6 +429,7 @@ export def install [ nu_plugin_example, nu_plugin_custom_values, nu_plugin_formats, + nu_plugin_selinux ] for plugin in $plugins {