bws/command/
run.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::{
    collections::HashMap,
    io::{IsTerminal, Read},
    process,
};

use bitwarden::{
    secrets_manager::{
        secrets::{SecretIdentifiersByProjectRequest, SecretIdentifiersRequest, SecretsGetRequest},
        ClientSecretsExt,
    },
    Client,
};
use color_eyre::eyre::{bail, Result};
use itertools::Itertools;
use uuid::Uuid;
use which::which;

use crate::{
    util::{is_valid_posix_name, uuid_to_posix},
    ACCESS_TOKEN_KEY_VAR_NAME,
};

// Essential environment variables that should be preserved even when `--no-inherit-env` is used
const WINDOWS_ESSENTIAL_VARS: &[&str] = &["SystemRoot", "ComSpec", "windir"];

pub(crate) async fn run(
    client: Client,
    organization_id: Uuid,
    project_id: Option<Uuid>,
    uuids_as_keynames: bool,
    no_inherit_env: bool,
    shell: Option<String>,
    command: Vec<String>,
) -> Result<i32> {
    let is_windows = std::env::consts::OS == "windows";

    let shell = shell.unwrap_or_else(|| {
        if is_windows {
            "powershell".to_string()
        } else {
            "sh".to_string()
        }
    });

    if which(&shell).is_err() {
        bail!("Shell '{}' not found", shell);
    }

    let user_command = if command.is_empty() {
        if std::io::stdin().is_terminal() {
            bail!("No command provided");
        }

        let mut buffer = String::new();
        std::io::stdin().read_to_string(&mut buffer)?;
        buffer
    } else {
        command.join(" ")
    };

    let res = if let Some(project_id) = project_id {
        client
            .secrets()
            .list_by_project(&SecretIdentifiersByProjectRequest { project_id })
            .await?
    } else {
        client
            .secrets()
            .list(&SecretIdentifiersRequest { organization_id })
            .await?
    };

    let secret_ids = res.data.into_iter().map(|e| e.id).collect();
    let secrets = client
        .secrets()
        .get_by_ids(SecretsGetRequest { ids: secret_ids })
        .await?
        .data;

    if !uuids_as_keynames {
        if let Some(duplicate) = secrets.iter().map(|s| &s.key).duplicates().next() {
            bail!("Multiple secrets with name: '{}'. Use --uuids-as-keynames or use unique names for secrets", duplicate);
        }
    }

    let environment: HashMap<String, String> = secrets
        .into_iter()
        .map(|s| {
            if uuids_as_keynames {
                (uuid_to_posix(&s.id), s.value)
            } else {
                (s.key, s.value)
            }
        })
        .inspect(|(k, _)| {
            if !is_valid_posix_name(k) {
                eprintln!(
                    "Warning: secret '{}' does not have a POSIX-compliant name",
                    k
                );
            }
        })
        .collect();

    let mut command = process::Command::new(shell);
    command
        .arg("-c")
        .arg(&user_command)
        .stdout(process::Stdio::inherit())
        .stderr(process::Stdio::inherit());

    if no_inherit_env {
        let path = std::env::var("PATH").unwrap_or_else(|_| match is_windows {
            true => "C:\\Windows;C:\\Windows\\System32".to_string(),
            false => "/bin:/usr/bin".to_string(),
        });

        command.env_clear();

        // Preserve essential PowerShell environment variables on Windows
        if is_windows {
            for &var in WINDOWS_ESSENTIAL_VARS {
                if let Ok(value) = std::env::var(var) {
                    command.env(var, value);
                }
            }
        }

        command.env("PATH", path); // PATH is always necessary
        command.envs(environment);
    } else {
        command.env_remove(ACCESS_TOKEN_KEY_VAR_NAME);
        command.envs(environment);
    }

    // propagate the exit status from the child process
    match command.spawn() {
        Ok(mut child) => match child.wait() {
            Ok(exit_status) => Ok(exit_status.code().unwrap_or(1)),
            Err(e) => {
                bail!("Failed to wait for process: {}", e)
            }
        },
        Err(e) => {
            bail!("Failed to execute process: {}", e)
        }
    }
}