Autoenv rewrite, security and scripting (#2083)

* Add args in .nurc file to environment

* Working dummy version

* Add add_nurc to sync_env command

* Parse .nurc file

* Delete env vars after leaving directory

* Removing vals not working, strangely

* Refactoring, add comment

* Debugging

* Debug by logging to file

* Add and remove env var behavior appears correct

However, it does not use existing code that well.

* Move work to cli.rs

* Parse config directories

* I am in a state of distress

* Rename .nurc to .nu

* Some notes for me

* Refactoring

* Removing vars works, but not done in a very nice fashion

* Refactor env_vars_to_delete

* Refactor env_vars_to_add()

* Move directory environment code to separate file

* Refactor from_config

* Restore env values

* Working?

* Working?

* Update comments and change var name

* Formatting

* Remove vars after leaving dir

* Remove notes I made

* Rename config function

* Clippy

* Cleanup and handle errors

* cargo fmt

* Better error messages, remove last (?) unwrap

* FORMAT PLZ

* Rename whitelisted_directories to allowed_directories

* Add comment to clarify how overwritten values are restored.

* Change list of allowed dirs to indexmap

* Rewrite starting

* rewrite everything

* Overwritten env values tracks an indexmap instead of vector

* Refactor restore function

* Untrack removed vars properly

* Performance concerns

* Performance concerns

* Error handling

* Clippy

* Add type aliases for String and OsString

* Deletion almost works

* Working?

* Error handling and refactoring

* nicer errors

* Add TODO file

* Move outside of loop

* Error handling

* Reworking adding of vars

* Reworking adding of vars

* Ready for testing

* Refactoring

* Restore overwritten vals code

* todo.org

* Remove overwritten values tracking, as it is not needed

* Cleanup, stop tracking overwritten values as nu takes care of it

* Init autoenv command

* Initialize autoenv and autoenv trust

* autoenv trust toml

* toml

* Use serde for autoenv

* Optional directory arg

* Add autoenv untrust command

* ... actually add autoenv untrust this time

* OsString and paths

* Revert "OsString and paths"

This reverts commit e6eedf8824.

* Fix path

* Fix path

* Autoenv trust and untrust

* Start using autoenv

* Check hashes

* Use trust functionality when setting vars

* Remove unused code

* Clippy

* Nicer errors for autoenv commands

* Non-working errors

* Update error description

* Satisfy fmt

* Errors

* Errors print, but not nicely

* Nicer errors

* fmt

* Delete accidentally added todo.org file

* Rename direnv to autoenv

* Use ShellError instead of Error

* Change tests to pass, danger zone?

* Clippy and errors

* Clippy... again

* Replace match with or_else

* Use sha2 crate for hashing

* parsing and error msg

* Refactoring

* Only apply vars once

* if parent dir

* Delete vars

* Rework exit code

* Adding works

* restore

* Fix possibility of infinite loop

* Refactoring

* Non-working

* Revert "Non-working"

This reverts commit e231b85570.

* Revert "Revert "Non-working""

This reverts commit 804092e46a.

* Autoenv trust works without restart

* Cargo fix

* Script vars

* Serde

* Serde errors

* Entry and exitscripts

* Clippy

* Support windows and handle errors

* Formatting

* Fix infinite loop on windows

* Debugging windows loop

* More windows infinite loop debugging

* Windows loop debugging #3

* windows loop #4

* Don't return err

* Cleanup unused code

* Infinite loop debug

* Loop debugging

* Check if infinite loop is vars_to_add

* env_vars_to_add does not terminate, skip loop as test

* Hypothesis: std::env::current_dir() is messing with something

* Hypothesis: std::env::current_dir() is messing with something

* plz

* make clippy happy

* debugging in env_vars_to_add

* Debbuging env_vars_to_add #2

* clippy

* clippy..

* Fool clippy

* Fix another infinite loop

* Binary search for error location x)

* Binary search #3

* fmt

* Binary search #4

* more searching...

* closing in... maybe

* PLZ

* Cleanup

* Restore commented out functionality

* Handle case when user gives the directory "."

* fmt

* Use fs::canonicalize for paths

* Create optional script section

* fmt

* Add exitscripts even if no entryscripts are defined

* All sections in .nu-env are now optional

* Re-read config file each directory change

* Hot reload after autoenv untrust, don't run exitscripts if untrusted

* Debugging

* Fix issue with recursive adding of vars

* Thank you for finding my issues Mr. Azure

* use std::env
This commit is contained in:
Sam Hedin 2020-07-05 19:34:00 +02:00 committed by GitHub
parent 9e82e5a2fa
commit ee18f16378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 625 additions and 258 deletions

64
Cargo.lock generated
View File

@ -322,7 +322,16 @@ dependencies = [
"block-padding",
"byte-tools",
"byteorder",
"generic-array",
"generic-array 0.12.3",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array 0.14.2",
]
[[package]]
@ -634,6 +643,12 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "cpuid-bool"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d375c433320f6c5057ae04a04376eef4d04ce2801448cf8863a78da99107be4"
[[package]]
name = "crc32fast"
version = "1.2.0"
@ -866,7 +881,16 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
dependencies = [
"generic-array",
"generic-array 0.12.3",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array 0.14.2",
]
[[package]]
@ -1418,6 +1442,16 @@ dependencies = [
"typenum",
]
[[package]]
name = "generic-array"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
dependencies = [
"typenum",
"version_check 0.9.2",
]
[[package]]
name = "gethostname"
version = "0.2.1"
@ -2517,6 +2551,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"serde_yaml",
"sha2",
"shellexpand",
"starship",
"strip-ansi-escapes",
@ -2969,6 +3004,12 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "open"
version = "1.4.0"
@ -3892,10 +3933,23 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
dependencies = [
"block-buffer",
"digest",
"block-buffer 0.7.3",
"digest 0.8.1",
"fake-simd",
"opaque-debug",
"opaque-debug 0.2.3",
]
[[package]]
name = "sha2"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpuid-bool",
"digest 0.9.0",
"opaque-debug 0.3.0",
]
[[package]]

View File

@ -75,6 +75,7 @@ serde_ini = "0.2.0"
serde_json = "1.0.55"
serde_urlencoded = "0.6.1"
serde_yaml = "0.8"
sha2 = "0.9.1"
shellexpand = "2.0.0"
strip-ansi-escapes = "0.1.0"
tempfile = "3.1.0"

View File

@ -352,6 +352,9 @@ pub fn create_default_context(
whole_stream_command(Headers),
// Data processing
whole_stream_command(Histogram),
whole_stream_command(Autoenv),
whole_stream_command(AutoenvTrust),
whole_stream_command(AutoenvUnTrust),
whole_stream_command(Math),
whole_stream_command(MathAverage),
whole_stream_command(MathMedian),

View File

@ -8,6 +8,9 @@ pub(crate) mod alias;
pub(crate) mod ansi;
pub(crate) mod append;
pub(crate) mod args;
pub(crate) mod autoenv;
pub(crate) mod autoenv_trust;
pub(crate) mod autoenv_untrust;
pub(crate) mod autoview;
pub(crate) mod build_string;
pub(crate) mod cal;
@ -142,6 +145,9 @@ pub(crate) use command::{
pub(crate) use alias::Alias;
pub(crate) use ansi::Ansi;
pub(crate) use append::Append;
pub(crate) use autoenv::Autoenv;
pub(crate) use autoenv_trust::AutoenvTrust;
pub(crate) use autoenv_untrust::AutoenvUnTrust;
pub(crate) use build_string::BuildString;
pub(crate) use cal::Cal;
pub(crate) use calc::Calc;

View File

@ -0,0 +1,82 @@
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{ReturnSuccess, Signature, UntaggedValue};
use serde::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::io::Read;
use std::path::PathBuf;
pub struct Autoenv;
#[derive(Deserialize, Serialize, Debug, Default)]
pub struct Trusted {
pub files: IndexMap<String, Vec<u8>>,
}
impl Trusted {
pub fn new() -> Self {
Trusted {
files: IndexMap::new(),
}
}
}
pub fn file_is_trusted(nu_env_file: &PathBuf, content: &[u8]) -> Result<bool, ShellError> {
let contentdigest = Sha256::digest(&content).as_slice().to_vec();
let nufile = nu_env_file.to_str().unwrap_or("");
let trusted = read_trusted()?;
Ok(trusted.files.get(nufile) == Some(&contentdigest))
}
pub fn read_trusted() -> Result<Trusted, ShellError> {
let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?;
let mut file = std::fs::OpenOptions::new()
.read(true)
.create(true)
.write(true)
.open(config_path)
.or_else(|_| {
Err(ShellError::untagged_runtime_error(
"Couldn't open nu-env.toml",
))
})?;
let mut doc = String::new();
file.read_to_string(&mut doc)?;
let allowed = toml::de::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new());
Ok(allowed)
}
#[async_trait]
impl WholeStreamCommand for Autoenv {
fn name(&self) -> &str {
"autoenv"
}
fn usage(&self) -> &str {
// "Mark a .nu-env file in a directory as trusted. Needs to be re-run after each change to the file or its filepath."
"Manage directory specific environments"
}
fn signature(&self) -> Signature {
Signature::build("autoenv")
}
async fn run(
&self,
_args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let registry = registry.clone();
Ok(OutputStream::one(ReturnSuccess::value(
UntaggedValue::string(crate::commands::help::get_help(&Autoenv, &registry))
.into_value(Tag::unknown()),
)))
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Allow .nu-env file in current directory",
example: "autoenv trust",
result: None,
}]
}
}

View File

@ -0,0 +1,74 @@
use super::autoenv::read_trusted;
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::SyntaxShape;
use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
use sha2::{Digest, Sha256};
use std::{fs, path::PathBuf};
pub struct AutoenvTrust;
#[async_trait]
impl WholeStreamCommand for AutoenvTrust {
fn name(&self) -> &str {
"autoenv trust"
}
fn signature(&self) -> Signature {
Signature::build("autoenv trust").optional("dir", SyntaxShape::String, "Directory to allow")
}
fn usage(&self) -> &str {
"Trust a .nu-env file in the current or given directory"
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let file_to_trust = match args.call_info.evaluate(registry).await?.args.nth(0) {
Some(Value {
value: UntaggedValue::Primitive(Primitive::String(ref path)),
tag: _,
}) => {
let mut dir = fs::canonicalize(path)?;
dir.push(".nu-env");
dir
}
_ => {
let mut dir = std::env::current_dir()?;
dir.push(".nu-env");
dir
}
};
let content = std::fs::read(&file_to_trust)?;
let filename = file_to_trust.to_string_lossy().to_string();
let mut allowed = read_trusted()?;
allowed
.files
.insert(filename, Sha256::digest(&content).as_slice().to_vec());
let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?;
let tomlstr = toml::to_string(&allowed).or_else(|_| {
Err(ShellError::untagged_runtime_error(
"Couldn't serialize allowed dirs to nu-env.toml",
))
})?;
fs::write(config_path, tomlstr).expect("Couldn't write to toml file");
Ok(OutputStream::one(ReturnSuccess::value(
UntaggedValue::string(".nu-env trusted!").into_value(tag),
)))
}
fn is_binary(&self) -> bool {
false
}
fn examples(&self) -> Vec<Example> {
Vec::new()
}
}

View File

@ -0,0 +1,98 @@
use super::autoenv::Trusted;
use crate::commands::WholeStreamCommand;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::SyntaxShape;
use nu_protocol::{Primitive, ReturnSuccess, Signature, UntaggedValue, Value};
use std::io::Read;
use std::{fs, path::PathBuf};
pub struct AutoenvUnTrust;
#[async_trait]
impl WholeStreamCommand for AutoenvUnTrust {
fn name(&self) -> &str {
"autoenv untrust"
}
fn signature(&self) -> Signature {
Signature::build("autoenv untrust").optional(
"dir",
SyntaxShape::String,
"Directory to disallow",
)
}
fn usage(&self) -> &str {
"Untrust a .nu-env file in the current or given directory"
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let tag = args.call_info.name_tag.clone();
let file_to_untrust = match args.call_info.evaluate(registry).await?.args.nth(0) {
Some(Value {
value: UntaggedValue::Primitive(Primitive::String(ref path)),
tag: _,
}) => {
let mut dir = fs::canonicalize(path)?;
dir.push(".nu-env");
dir
}
_ => {
let mut dir = std::env::current_dir()?;
dir.push(".nu-env");
dir
}
};
let config_path = config::default_path_for(&Some(PathBuf::from("nu-env.toml")))?;
let mut file = match std::fs::OpenOptions::new()
.read(true)
.create(true)
.write(true)
.open(config_path.clone())
{
Ok(p) => p,
Err(_) => {
return Err(ShellError::untagged_runtime_error(
"Couldn't open nu-env.toml",
));
}
};
let mut doc = String::new();
file.read_to_string(&mut doc)?;
let mut allowed: Trusted = toml::from_str(doc.as_str()).unwrap_or_else(|_| Trusted::new());
let file_to_untrust = file_to_untrust.to_string_lossy().to_string();
if allowed.files.remove(&file_to_untrust).is_none() {
return
Err(ShellError::untagged_runtime_error(
"No .nu-env file to untrust in the given directory. Is it missing, or already untrusted?",
));
}
let tomlstr = toml::to_string(&allowed).or_else(|_| {
Err(ShellError::untagged_runtime_error(
"Couldn't serialize allowed dirs to nu-env.toml",
))
})?;
fs::write(config_path, tomlstr).expect("Couldn't write to toml file");
Ok(OutputStream::one(ReturnSuccess::value(
UntaggedValue::string(".nu-env untrusted!").into_value(tag),
)))
}
fn is_binary(&self) -> bool {
false
}
fn examples(&self) -> Vec<Example> {
Vec::new()
}
}

View File

@ -28,6 +28,10 @@ pub(crate) async fn run_internal_command(
let objects: InputStream = trace_stream!(target: "nu::trace_stream::internal", "input" = input);
let internal_command = context.expect_command(&command.name);
if command.name == "autoenv untrust" {
context.user_recently_used_autoenv_untrust = true;
}
let result = {
context
.run_command(

View File

@ -75,6 +75,7 @@ impl WholeStreamCommand for RunExternalCommand {
Context {
registry: registry.clone(),
host: args.host.clone(),
user_recently_used_autoenv_untrust: false,
shell_manager: args.shell_manager.clone(),
ctrl_c: args.ctrl_c.clone(),
current_errors: Arc::new(Mutex::new(vec![])),
@ -88,6 +89,7 @@ impl WholeStreamCommand for RunExternalCommand {
{
Context {
registry: registry.clone(),
user_recently_used_autoenv_untrust: false,
host: args.host.clone(),
shell_manager: args.shell_manager.clone(),
ctrl_c: args.ctrl_c.clone(),

View File

@ -76,6 +76,7 @@ pub struct Context {
pub current_errors: Arc<Mutex<Vec<ShellError>>>,
pub ctrl_c: Arc<AtomicBool>,
pub raw_input: String,
pub user_recently_used_autoenv_untrust: bool,
pub(crate) shell_manager: ShellManager,
#[cfg(windows)]
@ -96,6 +97,7 @@ impl Context {
current_errors: raw_args.current_errors.clone(),
ctrl_c: raw_args.ctrl_c.clone(),
shell_manager: raw_args.shell_manager.clone(),
user_recently_used_autoenv_untrust: false,
windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())),
raw_input: String::default(),
}
@ -108,6 +110,7 @@ impl Context {
current_errors: raw_args.current_errors.clone(),
ctrl_c: raw_args.ctrl_c.clone(),
shell_manager: raw_args.shell_manager.clone(),
user_recently_used_autoenv_untrust: false,
raw_input: String::default(),
}
}
@ -122,6 +125,7 @@ impl Context {
current_errors: args.current_errors.clone(),
ctrl_c: args.ctrl_c.clone(),
shell_manager: args.shell_manager.clone(),
user_recently_used_autoenv_untrust: false,
windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())),
raw_input: String::default(),
}
@ -133,6 +137,7 @@ impl Context {
host: args.host.clone(),
current_errors: args.current_errors.clone(),
ctrl_c: args.ctrl_c.clone(),
user_recently_used_autoenv_untrust: false,
shell_manager: args.shell_manager.clone(),
raw_input: String::default(),
}
@ -151,6 +156,7 @@ impl Context {
))),
current_errors: Arc::new(Mutex::new(vec![])),
ctrl_c: Arc::new(AtomicBool::new(false)),
user_recently_used_autoenv_untrust: false,
shell_manager: ShellManager::basic(registry)?,
windows_drives_previous_cwd: Arc::new(Mutex::new(std::collections::HashMap::new())),
raw_input: String::default(),
@ -166,6 +172,7 @@ impl Context {
))),
current_errors: Arc::new(Mutex::new(vec![])),
ctrl_c: Arc::new(AtomicBool::new(false)),
user_recently_used_autoenv_untrust: false,
shell_manager: ShellManager::basic(registry)?,
raw_input: String::default(),
})

View File

@ -4,7 +4,6 @@ use std::fmt::Debug;
pub trait Conf: Debug + Send {
fn env(&self) -> Option<Value>;
fn path(&self) -> Option<Value>;
fn nu_env_dirs(&self) -> Option<Value>;
fn reload(&self);
}
@ -13,10 +12,6 @@ impl Conf for Box<dyn Conf> {
(**self).env()
}
fn nu_env_dirs(&self) -> Option<Value> {
(**self).nu_env_dirs()
}
fn path(&self) -> Option<Value> {
(**self).path()
}

View File

@ -20,10 +20,6 @@ impl Conf for NuConfig {
self.path()
}
fn nu_env_dirs(&self) -> Option<Value> {
self.nu_env_dirs()
}
fn reload(&self) {
let mut vars = self.vars.lock();
@ -56,14 +52,6 @@ impl NuConfig {
None
}
pub fn nu_env_dirs(&self) -> Option<Value> {
let vars = self.vars.lock();
if let Some(dirs) = vars.get("nu_env_dirs") {
return Some(dirs.clone());
}
None
}
pub fn path(&self) -> Option<Value> {
let vars = self.vars.lock();

View File

@ -16,10 +16,6 @@ impl Conf for FakeConfig {
self.config.env()
}
fn nu_env_dirs(&self) -> Option<Value> {
None
}
fn path(&self) -> Option<Value> {
self.config.path()
}

View File

@ -1,192 +1,238 @@
use indexmap::IndexMap;
use nu_protocol::{Primitive, UntaggedValue, Value};
use std::io::{Error, ErrorKind, Result};
use std::{ffi::OsString, fmt::Debug, path::PathBuf};
use crate::commands;
use commands::autoenv;
use indexmap::{IndexMap, IndexSet};
use nu_errors::ShellError;
use serde::Deserialize;
use std::cmp::Ordering::Less;
use std::env::*;
use std::process::Command;
use std::{
ffi::OsString,
fmt::Debug,
path::{Path, PathBuf},
};
type EnvKey = String;
type EnvVal = OsString;
#[derive(Debug, Default)]
pub struct DirectorySpecificEnvironment {
allowed_directories: Vec<PathBuf>,
pub last_seen_directory: PathBuf,
//If an environment var has been added from a .nu in a directory, we track it here so we can remove it when the user leaves the directory.
//If setting the var overwrote some value, we save the old value in an option so we can restore it later.
added_env_vars: IndexMap<PathBuf, IndexMap<EnvKey, Option<EnvVal>>>,
exitscripts: IndexMap<PathBuf, Vec<String>>,
}
//Directory -> Env key. If an environment var has been added from a .nu in a directory, we track it here so we can remove it when the user leaves the directory.
added_env_vars: IndexMap<PathBuf, Vec<String>>,
//Directory -> (env_key, value). If a .nu overwrites some existing environment variables, they are added here so that they can be restored later.
overwritten_env_values: IndexMap<PathBuf, Vec<(String, OsString)>>,
#[derive(Deserialize, Debug, Default)]
pub struct NuEnvDoc {
pub env: Option<IndexMap<String, String>>,
pub scriptvars: Option<IndexMap<String, String>>,
pub scripts: Option<IndexMap<String, Vec<String>>>,
pub entryscripts: Option<Vec<String>>,
pub exitscripts: Option<Vec<String>>,
}
impl DirectorySpecificEnvironment {
pub fn new(allowed_directories: Option<Value>) -> DirectorySpecificEnvironment {
let mut allowed_directories = if let Some(Value {
value: UntaggedValue::Table(ref wrapped_directories),
..
}) = allowed_directories
{
wrapped_directories
.iter()
.filter_map(|dirval| {
if let Value {
value: UntaggedValue::Primitive(Primitive::String(ref dir)),
..
} = dirval
{
return Some(PathBuf::from(&dir));
}
None
})
.collect()
pub fn new() -> DirectorySpecificEnvironment {
let root_dir = if cfg!(target_os = "windows") {
PathBuf::from("c:\\")
} else {
vec![]
PathBuf::from("/")
};
allowed_directories.sort();
DirectorySpecificEnvironment {
allowed_directories,
last_seen_directory: root_dir,
added_env_vars: IndexMap::new(),
overwritten_env_values: IndexMap::new(),
exitscripts: IndexMap::new(),
}
}
//If we are no longer in a directory, we restore the values it overwrote.
pub fn overwritten_values_to_restore(&mut self) -> Result<IndexMap<String, String>> {
let current_dir = std::env::current_dir()?;
fn toml_if_directory_is_trusted(
&mut self,
nu_env_file: &PathBuf,
) -> Result<NuEnvDoc, ShellError> {
let content = std::fs::read(&nu_env_file)?;
let mut keyvals_to_restore = IndexMap::new();
let mut new_overwritten = IndexMap::new();
if autoenv::file_is_trusted(&nu_env_file, &content)? {
let mut doc: NuEnvDoc = toml::de::from_slice(&content)
.or_else(|e| Err(ShellError::untagged_runtime_error(format!("{:?}", e))))?;
for (directory, keyvals) in &self.overwritten_env_values {
let mut working_dir = Some(current_dir.as_path());
let mut re_add_keyvals = true;
while let Some(wdir) = working_dir {
if wdir == directory.as_path() {
re_add_keyvals = false;
new_overwritten.insert(directory.clone(), keyvals.clone());
break;
} else {
working_dir = working_dir //Keep going up in the directory structure with .parent()
.ok_or_else(|| {
Error::new(ErrorKind::NotFound, "Root directory has no parent")
})?
.parent();
}
}
if re_add_keyvals {
for (k, v) in keyvals {
keyvals_to_restore.insert(
k.clone(),
v.to_str()
.ok_or_else(|| {
Error::new(
ErrorKind::Other,
format!("{:?} is not valid unicode", v),
)
})?
.to_string(),
);
}
}
}
self.overwritten_env_values = new_overwritten;
Ok(keyvals_to_restore)
}
pub fn env_vars_to_add(&mut self) -> Result<IndexMap<String, String>> {
let current_dir = std::env::current_dir()?;
let mut vars_to_add = IndexMap::new();
for dir in &self.allowed_directories {
let mut working_dir = Some(current_dir.as_path());
//Start in the current directory, then traverse towards the root with working_dir to see if we are in a subdirectory of a valid directory.
while let Some(wdir) = working_dir {
if wdir == dir.as_path() {
let toml_doc = std::fs::read_to_string(wdir.join(".nu-env").as_path())?
.parse::<toml::Value>()?;
let vars_in_current_file = toml_doc
.get("env")
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"No [env] section in .nu-env",
)
})?
.as_table()
.ok_or_else(|| {
Error::new(ErrorKind::InvalidData, "Malformed [env] section in .nu-env")
})?;
let mut keys_in_current_nufile = vec![];
for (k, v) in vars_in_current_file {
vars_to_add.insert(
k.clone(),
v.as_str()
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidData,
format!("Could not read environment variable: {}", v),
)
})?
.to_string(),
); //This is used to add variables to the environment
keys_in_current_nufile.push(k.clone()); //this is used to keep track of which directory added which variables
if let Some(scripts) = doc.scripts.as_ref() {
for (k, v) in scripts {
if k == "entryscripts" {
doc.entryscripts = Some(v.clone());
} else if k == "exitscripts" {
doc.exitscripts = Some(v.clone());
}
//If we are about to overwrite any environment variables, we save them first so they can be restored later.
self.overwritten_env_values.insert(
wdir.to_path_buf(),
keys_in_current_nufile
.iter()
.filter_map(|key| {
if let Some(val) = std::env::var_os(key) {
return Some((key.clone(), val));
}
None
})
.collect(),
);
self.added_env_vars
.insert(wdir.to_path_buf(), keys_in_current_nufile);
break;
} else {
working_dir = working_dir //Keep going up in the directory structure with .parent()
.ok_or_else(|| {
Error::new(ErrorKind::NotFound, "Root directory has no parent")
})?
.parent();
}
}
return Ok(doc);
}
Err(ShellError::untagged_runtime_error(
format!("{:?} is untrusted. Run 'autoenv trust {:?}' to trust it.\nThis needs to be done after each change to the file.", nu_env_file, nu_env_file.parent().unwrap_or_else(|| &Path::new("")))))
}
pub fn env_vars_to_add(&mut self) -> Result<IndexMap<EnvKey, EnvVal>, ShellError> {
let mut dir = current_dir()?;
let mut vars_to_add: IndexMap<EnvKey, EnvVal> = IndexMap::new();
//If we are in the last seen directory, do nothing
//If we are in a parent directory to last_seen_directory, just return without applying .nu-env in the parent directory - they were already applied earlier.
//parent.cmp(child) = Less
let mut popped = true;
while self.last_seen_directory.cmp(&dir) == Less && popped {
let nu_env_file = dir.join(".nu-env");
if nu_env_file.exists() {
let nu_env_doc = self.toml_if_directory_is_trusted(&nu_env_file)?;
//add regular variables from the [env section]
if let Some(env) = nu_env_doc.env {
for (env_key, env_val) in env {
self.add_key_if_appropriate(&mut vars_to_add, &dir, &env_key, &env_val);
}
}
//Add variables that need to evaluate scripts to run, from [scriptvars] section
if let Some(scriptvars) = nu_env_doc.scriptvars {
for (env_key, dir_val_script) in scriptvars {
let command = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", dir_val_script.as_str()])
.output()?
} else {
Command::new("sh").arg("-c").arg(&dir_val_script).output()?
};
if command.stdout.is_empty() {
return Err(ShellError::untagged_runtime_error(format!(
"{:?} in {:?} did not return any output",
dir_val_script, dir
)));
}
let response =
std::str::from_utf8(&command.stdout[..command.stdout.len() - 1])
.or_else(|e| {
Err(ShellError::untagged_runtime_error(format!(
"Couldn't parse stdout from command {:?}: {:?}",
command, e
)))
})?;
self.add_key_if_appropriate(
&mut vars_to_add,
&dir,
&env_key,
&response.to_string(),
);
}
}
if let Some(entryscripts) = nu_env_doc.entryscripts {
for script in entryscripts {
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", script.as_str()])
.output()?;
} else {
Command::new("sh").arg("-c").arg(script).output()?;
}
}
}
if let Some(exitscripts) = nu_env_doc.exitscripts {
self.exitscripts.insert(dir.clone(), exitscripts);
}
}
popped = dir.pop();
}
Ok(vars_to_add)
}
//If the user has left directories which added env vars through .nu, we clear those vars
pub fn env_vars_to_delete(&mut self) -> Result<Vec<String>> {
let current_dir = std::env::current_dir()?;
pub fn add_key_if_appropriate(
&mut self,
vars_to_add: &mut IndexMap<EnvKey, EnvVal>,
dir: &PathBuf,
env_key: &str,
env_val: &str,
) {
//This condition is to make sure variables in parent directories don't overwrite variables set by subdirectories.
if !vars_to_add.contains_key(env_key) {
vars_to_add.insert(env_key.to_string(), OsString::from(env_val));
self.added_env_vars
.entry(dir.clone())
.or_insert(IndexMap::new())
.insert(env_key.to_string(), var_os(env_key));
}
}
//Gather up all environment variables that should be deleted.
//If we are not in a directory or one of its subdirectories, mark the env_vals it maps to for removal.
let vars_to_delete = self.added_env_vars.iter().fold(
Vec::new(),
|mut vars_to_delete, (directory, env_vars)| {
let mut working_dir = Some(current_dir.as_path());
pub fn cleanup_after_dir_exit(
&mut self,
) -> Result<IndexMap<EnvKey, Option<EnvVal>>, ShellError> {
let current_dir = current_dir()?;
let mut vars_to_cleanup = IndexMap::new();
while let Some(wdir) = working_dir {
if wdir == directory {
return vars_to_delete;
//If we are in the same directory as last_seen, or a subdirectory to it, do nothing
//If we are in a subdirectory to last seen, do nothing
//If we are in a parent directory to last seen, exit .nu-envs from last seen to parent and restore old vals
let mut dir = self.last_seen_directory.clone();
let mut popped = true;
while current_dir.cmp(&dir) == Less && popped {
if let Some(vars_added_by_this_directory) = self.added_env_vars.get(&dir) {
for (k, v) in vars_added_by_this_directory {
vars_to_cleanup.insert(k.clone(), v.clone());
}
self.added_env_vars.remove(&dir);
}
if let Some(scripts) = self.exitscripts.get(&dir) {
for script in scripts {
if cfg!(target_os = "windows") {
Command::new("cmd")
.args(&["/C", script.as_str()])
.output()?;
} else {
working_dir = working_dir.expect("Root directory has no parent").parent();
Command::new("sh").arg("-c").arg(script).output()?;
}
}
//only delete vars from directories we are not in
vars_to_delete.extend(env_vars.clone());
vars_to_delete
},
);
}
popped = dir.pop();
}
Ok(vars_to_cleanup)
}
Ok(vars_to_delete)
// If the user recently ran autoenv untrust on a file, we clear the environment variables it set and make sure to not run any possible exitscripts.
pub fn clear_recently_untrusted_file(&mut self) -> Result<(), ShellError> {
// Figure out which file was untrusted
// Remove all vars set by it
let current_trusted_files: IndexSet<PathBuf> = autoenv::read_trusted()?
.files
.iter()
.map(|(k, _)| PathBuf::from(k))
.collect();
// We figure out which file(s) the user untrusted by taking the set difference of current trusted files in .config/nu/nu-env.toml and the files tracked by self.added_env_vars
// If a file is in self.added_env_vars but not in nu-env.toml, it was just untrusted.
let untrusted_files: IndexSet<PathBuf> = self
.added_env_vars
.iter()
.filter_map(|(path, _)| {
if !current_trusted_files.contains(path) {
return Some(path.clone());
}
None
})
.collect();
for path in untrusted_files {
if let Some(added_keys) = self.added_env_vars.get(&path) {
for (key, _) in added_keys {
remove_var(key);
}
}
self.exitscripts.remove(&path);
self.added_env_vars.remove(&path);
}
Ok(())
}
}

View File

@ -1,15 +1,18 @@
use crate::data::config::Conf;
use crate::env::directory_specific_environment::*;
use indexmap::{indexmap, IndexSet};
use nu_errors::ShellError;
use nu_protocol::{UntaggedValue, Value};
use std::env::*;
use std::ffi::OsString;
use std::fmt::Debug;
pub trait Env: Debug + Send {
fn env(&self) -> Option<Value>;
fn path(&self) -> Option<Value>;
fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool);
fn add_env(&mut self, key: &str, value: &str);
fn add_path(&mut self, new_path: OsString);
}
@ -22,8 +25,8 @@ impl Env for Box<dyn Env> {
(**self).path()
}
fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool) {
(**self).add_env(key, value, overwrite_existing);
fn add_env(&mut self, key: &str, value: &str) {
(**self).add_env(key, value);
}
fn add_path(&mut self, new_path: OsString) {
@ -35,7 +38,7 @@ impl Env for Box<dyn Env> {
pub struct Environment {
environment_vars: Option<Value>,
path_vars: Option<Value>,
pub direnv: DirectorySpecificEnvironment,
pub autoenv: DirectorySpecificEnvironment,
}
impl Environment {
@ -43,49 +46,41 @@ impl Environment {
Environment {
environment_vars: None,
path_vars: None,
direnv: DirectorySpecificEnvironment::new(None),
autoenv: DirectorySpecificEnvironment::new(),
}
}
pub fn from_config<T: Conf>(configuration: &T) -> Environment {
let env = configuration.env();
let path = configuration.path();
Environment {
environment_vars: env,
path_vars: path,
direnv: DirectorySpecificEnvironment::new(configuration.nu_env_dirs()),
autoenv: DirectorySpecificEnvironment::new(),
}
}
pub fn maintain_directory_environment(&mut self) -> std::io::Result<()> {
self.direnv.env_vars_to_delete()?.iter().for_each(|k| {
self.remove_env(&k);
});
self.direnv.env_vars_to_add()?.iter().for_each(|(k, v)| {
self.add_env(&k, &v, true);
});
pub fn autoenv(&mut self, reload_trusted: bool) -> Result<(), ShellError> {
for (k, v) in self.autoenv.env_vars_to_add()? {
set_var(&k, OsString::from(v.to_string_lossy().to_string()));
}
self.direnv
.overwritten_values_to_restore()?
.iter()
.for_each(|(k, v)| {
self.add_env(&k, &v, true);
});
for (k, v) in self.autoenv.cleanup_after_dir_exit()? {
if let Some(v) = v {
set_var(k, v);
} else {
remove_var(k);
}
}
if reload_trusted {
self.autoenv.clear_recently_untrusted_file()?;
}
self.autoenv.last_seen_directory = current_dir()?;
Ok(())
}
fn remove_env(&mut self, key: &str) {
if let Some(Value {
value: UntaggedValue::Row(ref mut envs),
..
}) = self.environment_vars
{
envs.entries.remove(key);
};
}
pub fn morph<T: Conf>(&mut self, configuration: &T) {
self.environment_vars = configuration.env();
self.path_vars = configuration.path();
@ -109,7 +104,7 @@ impl Env for Environment {
None
}
fn add_env(&mut self, key: &str, value: &str, overwrite_existing: bool) {
fn add_env(&mut self, key: &str, value: &str) {
let value = UntaggedValue::string(value);
let new_envs = {
@ -120,7 +115,7 @@ impl Env for Environment {
{
let mut new_envs = envs.clone();
if !new_envs.contains_key(key) || overwrite_existing {
if !new_envs.contains_key(key) {
new_envs.insert_data_at_key(key, value.into_value(tag.clone()));
}
@ -146,7 +141,7 @@ impl Env for Environment {
{
let mut new_paths = current_paths.clone();
let new_path_candidates = std::env::split_paths(&paths).map(|path| {
let new_path_candidates = split_paths(&paths).map(|path| {
UntaggedValue::string(path.to_string_lossy()).into_value(tag.clone())
});
@ -238,7 +233,7 @@ mod tests {
let fake_config = FakeConfig::new(&file);
let mut actual = Environment::from_config(&fake_config);
actual.add_env("USER", "NUNO", false);
actual.add_env("USER", "NUNO");
assert_eq!(
actual.env(),
@ -271,7 +266,7 @@ mod tests {
let fake_config = FakeConfig::new(&file);
let mut actual = Environment::from_config(&fake_config);
actual.add_env("SHELL", "/usr/bin/sh", false);
actual.add_env("SHELL", "/usr/bin/sh");
assert_eq!(
actual.env(),

View File

@ -1,6 +1,8 @@
use crate::context::Context;
use crate::data::config::{Conf, NuConfig};
use crate::env::environment::{Env, Environment};
use nu_source::Text;
use parking_lot::Mutex;
use std::sync::Arc;
@ -44,14 +46,16 @@ impl EnvironmentSyncer {
pub fn sync_env_vars(&mut self, ctx: &mut Context) {
let mut environment = self.env.lock();
if let Err(e) = environment.autoenv(ctx.user_recently_used_autoenv_untrust) {
crate::cli::print_err(e, &Text::from(""));
}
ctx.user_recently_used_autoenv_untrust = false;
if environment.env().is_some() {
for (name, value) in ctx.with_host(|host| host.vars()) {
if name != "path" && name != "PATH" {
// account for new env vars present in the current session
// that aren't loaded from config.
environment.add_env(&name, &value, false);
environment.maintain_directory_environment().ok();
environment.add_env(&name, &value);
// clear the env var from the session
// we are about to replace them
@ -126,6 +130,7 @@ mod tests {
use crate::context::Context;
use crate::data::config::tests::FakeConfig;
use crate::env::environment::Env;
use indexmap::IndexMap;
use nu_errors::ShellError;
use nu_test_support::fs::Stub::FileWithContent;
use nu_test_support::playground::Playground;
@ -139,13 +144,12 @@ mod tests {
let mut ctx = Context::basic()?;
ctx.host = Arc::new(Mutex::new(Box::new(crate::env::host::FakeHost::new())));
let expected = vec![
(
"SHELL".to_string(),
"/usr/bin/you_already_made_the_nu_choice".to_string(),
),
("USER".to_string(), "NUNO".to_string()),
];
let mut expected = IndexMap::new();
expected.insert(
"SHELL".to_string(),
"/usr/bin/you_already_made_the_nu_choice".to_string(),
);
expected.insert("USER".to_string(), "NUNO".to_string());
Playground::setup("syncs_env_test_1", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
@ -203,33 +207,34 @@ mod tests {
.into_string()
.expect("Couldn't convert to string.");
let actual = vec![
("SHELL".to_string(), var_shell),
("USER".to_string(), var_user),
];
let mut found = IndexMap::new();
found.insert("SHELL".to_string(), var_shell);
found.insert("USER".to_string(), var_user);
assert_eq!(actual, expected);
for k in found.keys() {
assert!(expected.contains_key(k));
}
});
// Now confirm in-memory environment variables synced appropriately
// including the newer one accounted for.
let environment = actual.env.lock();
let vars = environment
let mut vars = IndexMap::new();
environment
.env()
.expect("No variables in the environment.")
.row_entries()
.map(|(name, value)| {
(
.for_each(|(name, value)| {
vars.insert(
name.to_string(),
value.as_string().expect("Couldn't convert to string"),
)
})
.collect::<Vec<_>>();
assert_eq!(vars, expected);
);
});
for k in expected.keys() {
assert!(vars.contains_key(k));
}
});
Ok(())
}
@ -238,10 +243,11 @@ mod tests {
let mut ctx = Context::basic()?;
ctx.host = Arc::new(Mutex::new(Box::new(crate::env::host::FakeHost::new())));
let expected = vec![(
let mut expected = IndexMap::new();
expected.insert(
"SHELL".to_string(),
"/usr/bin/you_already_made_the_nu_choice".to_string(),
)];
);
Playground::setup("syncs_env_test_2", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContent(
@ -278,26 +284,30 @@ mod tests {
.into_string()
.expect("Couldn't convert to string.");
let actual = vec![("SHELL".to_string(), var_shell)];
let mut found = IndexMap::new();
found.insert("SHELL".to_string(), var_shell);
assert_eq!(actual, expected);
for k in found.keys() {
assert!(expected.contains_key(k));
}
});
let environment = actual.env.lock();
let vars = environment
let mut vars = IndexMap::new();
environment
.env()
.expect("No variables in the environment.")
.row_entries()
.map(|(name, value)| {
(
.for_each(|(name, value)| {
vars.insert(
name.to_string(),
value.as_string().expect("Couldn't convert to string"),
)
})
.collect::<Vec<_>>();
assert_eq!(vars, expected);
value.as_string().expect("couldn't convert to string"),
);
});
for k in expected.keys() {
assert!(vars.contains_key(k));
}
});
Ok(())

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -1,5 +1,8 @@
mod alias;
mod append;
mod autoenv;
mod autoenv_trust;
mod autoenv_untrust;
mod cal;
mod calc;
mod cd;