Skip to content

Commit 12f9d88

Browse files
committed
fix: parse quoted gcp auth-provider command args
Signed-off-by: immanuwell <pchpr.00@list.ru>
1 parent 1f5a46b commit 12f9d88

3 files changed

Lines changed: 47 additions & 37 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ serde = "1.0.221"
7474
serde_json = "1.0.127"
7575
serde-saphyr = "0.0.26"
7676
serde-value = "0.7.0"
77+
shell-words = "1.1.1"
7778
syn = "2.0.98"
7879
tame-oauth = "0.10.0"
7980
tempfile = "3.1.0"

kube-client/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ tower-http = { workspace = true, features = ["auth", "map-response-body", "trace
7272
hyper-timeout = { workspace = true, optional = true }
7373
tame-oauth = { workspace = true, features = ["gcp"], optional = true }
7474
secrecy = { workspace = true }
75+
shell-words.workspace = true
7576
tracing = { workspace = true, features = ["log"], optional = true }
7677
hyper-openssl = { workspace = true, features = ["client-legacy", "tokio"], optional = true }
7778
form_urlencoded = { workspace = true, optional = true }

kube-client/src/client/auth/mod.rs

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -442,20 +442,24 @@ fn token_from_gcp_provider(provider: &AuthProviderConfig) -> Result<ProviderToke
442442
// NB: This property does currently not exist upstream in client-go
443443
// See https://github.com/kube-rs/kube/issues/1060
444444
let drop_env = provider.config.get("cmd-drop-env").cloned().unwrap_or_default();
445-
// TODO splitting args by space is not safe
445+
let drop_env = shell_words::split(&drop_env)
446+
.map_err(|e| Error::AuthExec(format!("invalid cmd-drop-env: {e}")))?;
447+
let params = shell_words::split(&params)
448+
.map_err(|e| Error::AuthExec(format!("invalid cmd-args: {e}")))?;
449+
let command_line = params.join(" ");
446450
let mut command = Command::new(cmd);
447451
// Do not pass the following env vars to the command
448-
for env in drop_env.trim().split(' ') {
452+
for env in drop_env {
449453
command.env_remove(env);
450454
}
451455
let output = command
452-
.args(params.trim().split(' '))
456+
.args(params)
453457
.output()
454458
.map_err(|e| Error::AuthExec(format!("Executing {cmd:} failed: {e:?}")))?;
455459

456460
if !output.status.success() {
457461
return Err(Error::AuthExecRun {
458-
cmd: format!("{cmd} {params}"),
462+
cmd: format!("{cmd} {command_line}"),
459463
status: output.status,
460464
out: output,
461465
});
@@ -646,43 +650,25 @@ fn auth_exec(auth: &ExecConfig) -> Result<ExecCredential, Error> {
646650

647651
#[cfg(test)]
648652
mod test {
649-
use crate::config::Kubeconfig;
650653
use std::time::{Duration, Instant};
651654

652655
use super::*;
653656

654657
/// Build an `AuthInfo` whose gcp auth-provider runs `cmd_path cmd_args` to emit credential JSON.
655658
fn gcp_auth_info(cmd_path: &str, cmd_args: &str) -> AuthInfo {
656-
let test_file = format!(
657-
r#"
658-
apiVersion: v1
659-
clusters:
660-
- cluster:
661-
certificate-authority-data: XXXXXXX
662-
server: https://36.XXX.XXX.XX
663-
name: generic-name
664-
contexts:
665-
- context:
666-
cluster: generic-name
667-
user: generic-name
668-
name: generic-name
669-
current-context: generic-name
670-
kind: Config
671-
preferences: {{}}
672-
users:
673-
- name: generic-name
674-
user:
675-
auth-provider:
676-
config:
677-
cmd-args: {cmd_args}
678-
cmd-path: {cmd_path}
679-
expiry-key: '{{.credential.token_expiry}}'
680-
token-key: '{{.credential.access_token}}'
681-
name: gcp
682-
"#
683-
);
684-
let config: Kubeconfig = serde_saphyr::from_str(&test_file).unwrap();
685-
config.auth_infos[0].auth_info.clone().unwrap()
659+
AuthInfo {
660+
auth_provider: Some(AuthProviderConfig {
661+
name: "gcp".into(),
662+
config: std::collections::HashMap::from([
663+
("cmd-args".into(), cmd_args.into()),
664+
("cmd-path".into(), cmd_path.into()),
665+
("expiry-key".into(), "{.credential.token_expiry}".into()),
666+
("token-key".into(), "{.credential.access_token}".into()),
667+
]),
668+
..Default::default()
669+
}),
670+
..Default::default()
671+
}
686672
}
687673

688674
fn cred_json(token: &str, expiry: &str) -> String {
@@ -695,7 +681,7 @@ mod test {
695681
#[ignore = "fails on windows mysteriously"]
696682
async fn exec_auth_command() -> Result<(), Error> {
697683
let expiry = (Timestamp::now() + SIXTY_SEC).to_string();
698-
let auth_info = gcp_auth_info("echo", &format!("'{}'", cred_json("my_token", &expiry)));
684+
let auth_info = gcp_auth_info("printf", &format!("%s '{}'", cred_json("my_token", &expiry)));
699685
match Auth::try_from(&auth_info).unwrap() {
700686
Auth::RefreshableToken(RefreshableToken::Exec(refreshable)) => {
701687
let (token, _expire, info) = Arc::try_unwrap(refreshable).unwrap().into_inner();
@@ -712,7 +698,7 @@ mod test {
712698
#[ignore = "shells out to echo/sh; skipped on windows"]
713699
async fn exec_token_refresh_via_to_header() -> Result<(), Error> {
714700
let fresh_expiry = (Timestamp::now() + SignedDuration::from_secs(3600)).to_string();
715-
let auth_info = gcp_auth_info("echo", &format!("'{}'", cred_json("my_token", &fresh_expiry)));
701+
let auth_info = gcp_auth_info("printf", &format!("%s '{}'", cred_json("my_token", &fresh_expiry)));
716702
// Seed with a stale token + past expiry to force the refresh branch.
717703
let stale_expiry = Timestamp::now() - SIXTY_SEC;
718704
let refreshable = RefreshableToken::Exec(Arc::new(Mutex::new((
@@ -773,6 +759,28 @@ mod test {
773759
Ok(())
774760
}
775761

762+
#[tokio::test]
763+
#[ignore = "shells out to sh; skipped on windows"]
764+
async fn exec_auth_command_parses_quoted_cmd_args() -> Result<(), Error> {
765+
let auth_info = AuthInfo {
766+
auth_provider: Some(AuthProviderConfig {
767+
name: "gcp".into(),
768+
config: std::collections::HashMap::from([
769+
("cmd-path".into(), "sh".into()),
770+
("cmd-args".into(), "-c 'printf %s \"my token\"'".into()),
771+
]),
772+
..Default::default()
773+
}),
774+
..Default::default()
775+
};
776+
777+
match Auth::try_from(&auth_info).unwrap() {
778+
Auth::Bearer(token) => assert_eq!(token.expose_secret(), "my token"),
779+
_ => unreachable!(),
780+
}
781+
Ok(())
782+
}
783+
776784
#[test]
777785
fn token_file() {
778786
let file = tempfile::NamedTempFile::new().unwrap();

0 commit comments

Comments
 (0)